diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8ed1b0a4..de33e7c4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -4,6 +4,7 @@ on: push: branches: [ "**" ] pull_request: + workflow_dispatch: permissions: contents: read @@ -27,7 +28,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: "21" + java-version: "17" cache: gradle # 可选:更安全,防止 wrapper 被篡改(不想要可删) diff --git a/build.gradle b/build.gradle index a7463137..d2926215 100644 --- a/build.gradle +++ b/build.gradle @@ -59,11 +59,13 @@ subprojects { inputs.file "../gradle.properties" } afterEvaluate { - tasks.named("sourcesJar") { - duplicatesStrategy = DuplicatesStrategy.INCLUDE + def sourcesJar = tasks.findByName("sourcesJar") + if (sourcesJar != null) { + sourcesJar.duplicatesStrategy = DuplicatesStrategy.INCLUDE } - tasks.named("jar") { - duplicatesStrategy = DuplicatesStrategy.INCLUDE + def jarTask = tasks.findByName("jar") + if (jarTask != null) { + jarTask.duplicatesStrategy = DuplicatesStrategy.INCLUDE } } } diff --git a/common/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java b/common/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java new file mode 100644 index 00000000..3034a3c0 --- /dev/null +++ b/common/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java @@ -0,0 +1,12 @@ +package cn.coostack.cooparticlesapi.mixin.compat.iris; + +import net.irisshaders.iris.gl.framebuffer.GlFramebuffer; +import net.irisshaders.iris.pipeline.FinalPassRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(FinalPassRenderer.class) +public interface FinalPassRendererAccessor { + @Accessor + GlFramebuffer getColorHolder(); +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt index d8940602..5f5fd9a6 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt @@ -38,10 +38,8 @@ import cn.coostack.cooparticlesapi.utils.interpolator.data.InterpolatorRelativeL import com.mojang.serialization.Codec import net.minecraft.core.BlockPos import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.codec.ByteBufCodecs -import net.minecraft.network.codec.StreamCodec import net.minecraft.world.item.ItemStack import net.minecraft.world.level.block.Block import net.minecraft.world.level.block.state.BlockState @@ -58,7 +56,7 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap object CodecHelper { - val supposedTypes = ConcurrentHashMap>() + val supposedTypes = ConcurrentHashMap>() /** * 记录只能由 [RegistryFriendlyByteBuf] 驱动的 codec 类型。 @@ -70,19 +68,19 @@ object CodecHelper { init { CodecHelperJava.init() - register(Short::class.java, StreamCodec.of({ buf, i -> buf.writeShort(i.toInt()) }, { it.readShort() })) - register(Int::class.java, StreamCodec.of({ buf, i -> buf.writeInt(i) }, { it.readInt() })) - register(Long::class.java, StreamCodec.of({ buf, i -> buf.writeLong(i) }, { it.readLong() })) - register(LongArray::class.java, StreamCodec.of({ buf, i -> buf.writeLongArray(i) }, { it.readLongArray() })) - register(Float::class.java, StreamCodec.of({ buf, i -> buf.writeFloat(i) }, { it.readFloat() })) - register(Double::class.java, StreamCodec.of({ buf, i -> buf.writeDouble(i) }, { it.readDouble() })) - register(String::class.java, StreamCodec.of({ buf, i -> buf.writeUtf(i) }, { it.readUtf() })) - register(Byte::class.java, StreamCodec.of({ buf, i -> buf.writeByte(i.toInt()) }, { it.readByte() })) - register(Boolean::class.java, StreamCodec.of({ buf, i -> buf.writeBoolean(i) }, { it.readBoolean() })) - register(ByteArray::class.java, StreamCodec.of({ buf, i -> buf.writeByteArray(i) }, { it.readByteArray() })) + register(Short::class.java, CommonStreamCodec.of({ buf, i -> buf.writeShort(i.toInt()) }, { it.readShort() })) + register(Int::class.java, CommonStreamCodec.of({ buf, i -> buf.writeInt(i) }, { it.readInt() })) + register(Long::class.java, CommonStreamCodec.of({ buf, i -> buf.writeLong(i) }, { it.readLong() })) + register(LongArray::class.java, CommonStreamCodec.of({ buf, i -> buf.writeLongArray(i) }, { it.readLongArray() })) + register(Float::class.java, CommonStreamCodec.of({ buf, i -> buf.writeFloat(i) }, { it.readFloat() })) + register(Double::class.java, CommonStreamCodec.of({ buf, i -> buf.writeDouble(i) }, { it.readDouble() })) + register(String::class.java, CommonStreamCodec.of({ buf, i -> buf.writeUtf(i) }, { it.readUtf() })) + register(Byte::class.java, CommonStreamCodec.of({ buf, i -> buf.writeByte(i.toInt()) }, { it.readByte() })) + register(Boolean::class.java, CommonStreamCodec.of({ buf, i -> buf.writeBoolean(i) }, { it.readBoolean() })) + register(ByteArray::class.java, CommonStreamCodec.of({ buf, i -> buf.writeByteArray(i) }, { it.readByteArray() })) register(CooUniformValue::class.java, CooUniformValue.STREAM_CODEC) - register(Char::class.java, StreamCodec.of({ buf, i -> buf.writeChar(i.code) }, { it.readChar() })) - register(UUID::class.java, StreamCodec.of({ buf, i -> buf.writeUUID(i) }, { it.readUUID() })) + register(Char::class.java, CommonStreamCodec.of({ buf, i -> buf.writeChar(i.code) }, { it.readChar() })) + register(UUID::class.java, CommonStreamCodec.of({ buf, i -> buf.writeUUID(i) }, { it.readUUID() })) registerRegistry(ControlableParticleData::class.java, ControlableParticleData.PACKET_CODEC) registerRegistry(ControlableCParticleData::class.java, ControlableCParticleData.PACKET_CODEC) registerRegistry(CParticleTextureSource::class.java, CParticleTextureSource.STREAM_CODEC) @@ -90,7 +88,7 @@ object CodecHelper { register(CParticleColorCurve::class.java, CParticleColorCurve.STREAM_CODEC) register( CParticleUpdateMode::class.java, - StreamCodec.of( + CommonStreamCodec.of( { buf, mode -> buf.writeByte(mode.ordinal) }, { buf -> val ordinal = buf.readUnsignedByte().toInt() @@ -103,8 +101,8 @@ object CodecHelper { ) registerRegistry(CompositionEmittersData::class.java, CompositionEmittersData.PACKET_CODEC) registerRegistry(DisplayEntityEmittersData::class.java, DisplayEntityEmittersData.PACKET_CODEC) - register(Vector3f::class.java, StreamCodec.of({ buf, i -> buf.writeVector3f(i) }, { it.readVector3f() })) - register(Vector4f::class.java, StreamCodec.of({ buf, v -> + register(Vector3f::class.java, CommonStreamCodec.of({ buf, i -> buf.writeVector3f(i) }, { it.readVector3f() })) + register(Vector4f::class.java, CommonStreamCodec.of({ buf, v -> buf.writeFloat(v.x) buf.writeFloat(v.y) buf.writeFloat(v.z) @@ -112,15 +110,15 @@ object CodecHelper { }, { Vector4f(it.readFloat(), it.readFloat(), it.readFloat(), it.readFloat()) })) - register(Vec2::class.java, StreamCodec.of({ buf, i -> + register(Vec2::class.java, CommonStreamCodec.of({ buf, i -> buf.writeFloat(i.x) buf.writeFloat(i.y) }, { Vec2(it.readFloat(), it.readFloat()) })) - register(Vec3::class.java, StreamCodec.of({ buf, i -> buf.writeVec3(i) }, { it.readVec3() })) - register(Quaternionf::class.java, StreamCodec.of({ buf, q -> buf.writeQuaternion(q) }, { it.readQuaternion() })) - register(AABB::class.java, StreamCodec.of({ buf, i -> + register(Vec3::class.java, CommonStreamCodec.of({ buf, i -> buf.writeVec3(i) }, { it.readVec3() })) + register(Quaternionf::class.java, CommonStreamCodec.of({ buf, q -> buf.writeQuaternion(q) }, { it.readQuaternion() })) + register(AABB::class.java, CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.minX) buf.writeDouble(i.minY) buf.writeDouble(i.minZ) @@ -130,7 +128,7 @@ object CodecHelper { }, { AABB(it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble()) })) - register(HitBox::class.java, StreamCodec.of({ buf, i -> + register(HitBox::class.java, CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.x1) buf.writeDouble(i.y1) buf.writeDouble(i.z1) @@ -142,7 +140,7 @@ object CodecHelper { })) registerRegistry(ItemStack::class.java, ItemStack.OPTIONAL_STREAM_CODEC) register(SimpleRandomParticleData::class.java, SimpleRandomParticleData.PACKET_CODEC) - register(RelativeLocation::class.java, StreamCodec.of({ buf, r -> + register(RelativeLocation::class.java, CommonStreamCodec.of({ buf, r -> buf.apply { writeDouble(r.x) writeDouble(r.y) @@ -158,29 +156,29 @@ object CodecHelper { register(InterpolatorRelativeLocation::class.java, InterpolatorRelativeLocation.CODEC) register( DoubleRangeData::class.java, - StreamCodec.of({ buf, i -> buf.writeDouble(i.min); buf.writeDouble(i.max) }, { + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.min); buf.writeDouble(i.max) }, { DoubleRangeData(it.readDouble(), it.readDouble()) }) ) register( IntRangeData::class.java, - StreamCodec.of({ buf, i -> buf.writeInt(i.min); buf.writeInt(i.max) }, { + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.min); buf.writeInt(i.max) }, { IntRangeData(it.readInt(), it.readInt()) }) ) register( FloatRangeData::class.java, - StreamCodec.of({ buf, i -> buf.writeFloat(i.min); buf.writeFloat(i.max) }, { + CommonStreamCodec.of({ buf, i -> buf.writeFloat(i.min); buf.writeFloat(i.max) }, { FloatRangeData(it.readFloat(), it.readFloat()) }) ) register( BlockPos::class.java, - StreamCodec.of(BlockPos.STREAM_CODEC::encode, BlockPos.STREAM_CODEC::decode) + CommonStreamCodec.of({ buf, pos -> buf.writeBlockPos(pos) }, { buf -> buf.readBlockPos() }) ) register( BlockState::class.java, - StreamCodec.of({ buf, s -> + CommonStreamCodec.of({ buf, s -> ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY) .encode(buf, s) }, { buf -> @@ -190,7 +188,7 @@ object CodecHelper { ) register( ValueConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeDouble(i.targetNum.toDouble()) buf.writeDouble(i.current.toDouble()) @@ -201,7 +199,7 @@ object CodecHelper { ) register( ValueConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.speed.toDouble()) buf.writeDouble(i.targetNum.toDouble()) buf.writeDouble(i.current.toDouble()) @@ -212,7 +210,7 @@ object CodecHelper { ) register( DoubleConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeDouble(i.targetNum) buf.writeDouble(i.current) @@ -223,7 +221,7 @@ object CodecHelper { ) register( FloatConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeFloat(i.targetNum) buf.writeFloat(i.current) @@ -234,7 +232,7 @@ object CodecHelper { ) register( IntConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeInt(i.targetNum) buf.writeDouble(i.currentRaw) @@ -245,7 +243,7 @@ object CodecHelper { ) register( Vec3ConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeVec3(i.targetNum) buf.writeVec3(i.current) @@ -256,7 +254,7 @@ object CodecHelper { ) register( RelativeLocationConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeDouble(i.targetNum.x) buf.writeDouble(i.targetNum.y) @@ -273,7 +271,7 @@ object CodecHelper { ) register( Vector3fConstTimeAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.durationTick) buf.writeVector3f(i.targetNum) buf.writeVector3f(i.current) @@ -284,7 +282,7 @@ object CodecHelper { ) register( DoubleConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.speed) buf.writeDouble(i.targetNum) buf.writeDouble(i.current) @@ -295,7 +293,7 @@ object CodecHelper { ) register( FloatConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeFloat(i.speed) buf.writeFloat(i.targetNum) buf.writeFloat(i.current) @@ -306,7 +304,7 @@ object CodecHelper { ) register( IntConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.speed) buf.writeInt(i.targetNum) buf.writeDouble(i.currentRaw) @@ -317,7 +315,7 @@ object CodecHelper { ) register( Vec3ConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.speed) buf.writeVec3(i.targetNum) buf.writeVec3(i.current) @@ -328,7 +326,7 @@ object CodecHelper { ) register( RelativeLocationConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.speed) buf.writeDouble(i.targetNum.x) buf.writeDouble(i.targetNum.y) @@ -345,7 +343,7 @@ object CodecHelper { ) register( Vector3fConstSpeedAnimator::class.java, - StreamCodec.of({ buf, i -> + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.speed) buf.writeVector3f(i.targetNum) buf.writeVector3f(i.current) @@ -379,7 +377,7 @@ object CodecHelper { * @param codec 他的编解码器 */ @JvmStatic - fun register(type: Class, codec: StreamCodec) { + fun register(type: Class, codec: CommonStreamCodec) { supposedTypes[type.name] = codec registryRequiredTypes.remove(type.name) } @@ -397,7 +395,7 @@ object CodecHelper { @JvmStatic fun registerRegistry( type: Class, - codec: StreamCodec, + codec: CommonStreamCodec, ) { supposedTypes[type.name] = codec registryRequiredTypes.add(type.name) @@ -409,7 +407,7 @@ object CodecHelper { * @param type */ - fun codecOf(type: Type): StreamCodec { + fun codecOf(type: Type): CommonStreamCodec<*> { val codecType = normalizeCodecType(type) if (codecType is Class<*>) { @@ -450,11 +448,11 @@ object CodecHelper { * @return 接受 [RegistryFriendlyByteBuf] 的字段 codec */ @Suppress("UNCHECKED_CAST") - fun registryCodecOf(type: Type): StreamCodec { + fun registryCodecOf(type: Type): CommonStreamCodec<*> { val codecType = normalizeCodecType(type) if (codecType is Class<*>) { - return supposedTypes[codecType.name] as? StreamCodec + return supposedTypes[codecType.name] as? CommonStreamCodec<*> ?: throw IllegalArgumentException("不支持的类型: ${codecType.name}") } @@ -492,15 +490,15 @@ object CodecHelper { } @Suppress("UNCHECKED_CAST") - fun codecList(type: Type): StreamCodec { + fun codecList(type: Type): CommonStreamCodec<*> { if (type !is ParameterizedType) { throw IllegalArgumentException("List字段必须声明具体泛型: $type") } val elementType = type.actualTypeArguments[0] - val elementCodec = codecOf(elementType) as StreamCodec + val elementCodec = codecOf(elementType) as CommonStreamCodec - return StreamCodec.of>( + return CommonStreamCodec.of({ buf, value -> { buf, value -> buf.writeVarInt(value.size) value.forEach { element -> @@ -519,15 +517,15 @@ object CodecHelper { } @Suppress("UNCHECKED_CAST") - fun codecSet(type: Type): StreamCodec { + fun codecSet(type: Type): CommonStreamCodec<*> { if (type !is ParameterizedType) { throw IllegalArgumentException("Set字段必须声明具体泛型: $type") } val elementType = type.actualTypeArguments[0] - val elementCodec = codecOf(elementType) as StreamCodec + val elementCodec = codecOf(elementType) as CommonStreamCodec - return StreamCodec.of>( + return CommonStreamCodec.of({ buf, value -> { buf, value -> buf.writeVarInt(value.size) value.forEach { element -> @@ -546,17 +544,17 @@ object CodecHelper { } @Suppress("UNCHECKED_CAST") - fun codecMap(type: Type): StreamCodec { + fun codecMap(type: Type): CommonStreamCodec<*> { if (type !is ParameterizedType) { throw IllegalArgumentException("Map字段必须声明具体泛型: $type") } val keyType = type.actualTypeArguments[0] val valueType = type.actualTypeArguments[1] - val keyCodec = codecOf(keyType) as StreamCodec - val valueCodec = codecOf(valueType) as StreamCodec + val keyCodec = codecOf(keyType) as CommonStreamCodec + val valueCodec = codecOf(valueType) as CommonStreamCodec - return StreamCodec.of>( + return CommonStreamCodec.of({ buf, value -> { buf, value -> buf.writeVarInt(value.size) value.forEach { (key, mapValue) -> @@ -585,10 +583,10 @@ object CodecHelper { * @return registry-aware List codec */ @Suppress("UNCHECKED_CAST") - private fun registryCodecList(type: ParameterizedType): StreamCodec { + private fun registryCodecList(type: ParameterizedType): CommonStreamCodec<*> { val elementCodec = registryCodecOf(type.actualTypeArguments[0]) as - StreamCodec - return StreamCodec.of>( + CommonStreamCodec + return CommonStreamCodec.of({ buf, value -> { buf, value -> buf.writeVarInt(value.size) value.forEach { element -> @@ -612,10 +610,10 @@ object CodecHelper { * @return registry-aware Set codec */ @Suppress("UNCHECKED_CAST") - private fun registryCodecSet(type: ParameterizedType): StreamCodec { + private fun registryCodecSet(type: ParameterizedType): CommonStreamCodec<*> { val elementCodec = registryCodecOf(type.actualTypeArguments[0]) as - StreamCodec - return StreamCodec.of>( + CommonStreamCodec + return CommonStreamCodec.of({ buf, value -> { buf, value -> buf.writeVarInt(value.size) value.forEach { element -> @@ -641,12 +639,12 @@ object CodecHelper { * @return registry-aware Map codec */ @Suppress("UNCHECKED_CAST") - private fun registryCodecMap(type: ParameterizedType): StreamCodec { + private fun registryCodecMap(type: ParameterizedType): CommonStreamCodec<*> { val keyCodec = registryCodecOf(type.actualTypeArguments[0]) as - StreamCodec + CommonStreamCodec val valueCodec = registryCodecOf(type.actualTypeArguments[1]) as - StreamCodec - return StreamCodec.of>( + CommonStreamCodec + return CommonStreamCodec.of({ buf, value -> { buf, value -> buf.writeVarInt(value.size) value.forEach { (key, mapValue) -> diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CommonStreamCodec.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CommonStreamCodec.kt new file mode 100644 index 00000000..7cedffe6 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CommonStreamCodec.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.annotations.codec + +interface CommonStreamCodec { + fun decode(buf: Any): T + fun encode(buf: Any, value: T) + companion object { + fun of(dec: (Any) -> T, enc: (Any, T) -> Unit): CommonStreamCodec = + object : CommonStreamCodec { + override fun decode(buf: Any): T = dec(buf) + override fun encode(buf: Any, value: T) = enc(buf, value) + } + } +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt new file mode 100644 index 00000000..58bd65ab --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt @@ -0,0 +1,64 @@ +package cn.coostack.cooparticlesapi.compat.iris + +import kotlin.jvm.JvmStatic +import net.irisshaders.iris.gl.framebuffer.GlFramebuffer +import net.irisshaders.iris.targets.DepthTexture +import net.irisshaders.iris.targets.RenderTargets + +object CooIrisRenderState { + @JvmStatic + fun beginFrame() {} + + @JvmStatic + fun captureSceneDepth(depthTextureId: Int, width: Int, height: Int) {} + + @JvmStatic + fun captureTerrainDepth(textureId: Int, width: Int, height: Int) {} + + @JvmStatic + fun captureNoHandDepth(textureId: Int, width: Int, height: Int) {} + + @JvmStatic + fun captureFinalColor(colorAttachment0: Any?, framebufferId: Int, width: Int, height: Int) {} + + @JvmStatic + fun clear() {} + + @JvmStatic + fun clearFinalColor() {} + + @JvmStatic + fun snapshot(): IrisSnapshot = EmptySnapshot + + object EmptySnapshot : IrisSnapshot { + override fun terrainDepthTextureId(): Int = 0 + override fun terrainDepthWidth(): Int = 0 + override fun terrainDepthHeight(): Int = 0 + override fun sceneDepthTextureId(): Int = 0 + override fun sceneDepthWidth(): Int = 0 + override fun sceneDepthHeight(): Int = 0 + override fun noHandDepthTextureId(): Int = 0 + override fun noHandDepthWidth(): Int = 0 + override fun noHandDepthHeight(): Int = 0 + override fun finalColorTextureId(): Int = 0 + override fun finalColorWidth(): Int = 0 + override fun finalColorHeight(): Int = 0 + override fun finalColorFramebufferId(): Int = 0 + } +} + +interface IrisSnapshot { + fun terrainDepthTextureId(): Int + fun terrainDepthWidth(): Int + fun terrainDepthHeight(): Int + fun sceneDepthTextureId(): Int + fun sceneDepthWidth(): Int + fun sceneDepthHeight(): Int + fun noHandDepthTextureId(): Int + fun noHandDepthWidth(): Int + fun noHandDepthHeight(): Int + fun finalColorTextureId(): Int + fun finalColorWidth(): Int + fun finalColorHeight(): Int + fun finalColorFramebufferId(): Int +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleEmitterBridge.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleEmitterBridge.kt index bf616485..44592b36 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleEmitterBridge.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleEmitterBridge.kt @@ -417,16 +417,18 @@ object CParticleEmitterBridge { val base = index * ForceCommand.STRIDE when (val force = command.force) { is CParticleForce.Texture -> { - val slot = textureSlots.getOrPut(force.resource) { - resources.add(force.resource) + val resource = force.resource + val slot = textureSlots.getOrPut(resource) { + resources.add(resource) textureSlots.size } command.pack(packed, base, origin, slot) } is CParticleForce.FluidFlow -> { - val slot = fluidSlots.getOrPut(force.resource) { - resources.add(force.resource) + val resource = force.resource + val slot = fluidSlots.getOrPut(resource) { + resources.add(resource) fluidSlots.size } command.pack(packed, base, origin, slot) diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt new file mode 100644 index 00000000..2c9a6966 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +class CParticleFluidResource(id: String) : CParticleForceResource(id), CParticleFluidBinding { + override fun sampleFluid(x: Double, y: Double, z: Double, out: FloatArray) {} + fun bindCompute(index: Int) {} + fun resetCompute() {} +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt new file mode 100644 index 00000000..2230fadc --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +open class CParticleForceResource(val id: String) { + override fun equals(other: Any?): Boolean = other is CParticleForceResource && other.id == id + override fun hashCode(): Int = id.hashCode() +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceRegistry.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceRegistry.kt new file mode 100644 index 00000000..fc6292d4 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceRegistry.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +object CParticleForceResourceRegistry { + private val bindings = mutableMapOf() + fun bind(resource: CParticleForceResource, binding: Any) { + bindings[resource] = binding + } + fun clearResolvedBindings() { + bindings.clear() + } +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt new file mode 100644 index 00000000..4cd5a9ab --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +interface CParticleTextureBinding { + fun sampleTexture(x: Double, y: Double, z: Double, out: FloatArray) +} + +interface CParticleFluidBinding { + fun sampleFluid(x: Double, y: Double, z: Double, out: FloatArray) +} + +class CParticleForceResourceTable { + companion object { + const val MAX_TEXTURE_RESOURCES: Int = 16 + } + fun resolve(resource: CParticleForceResource): Any? = null + fun slotFor(resource: CParticleTextureResource): Int = -1 + fun slotFor(resource: CParticleFluidResource): Int = -1 + fun textureBinding(slot: Int): CParticleTextureBinding = NullTextureBinding + fun fluidBinding(slot: Int): CParticleFluidBinding = NullFluidBinding + fun textureBindings(): List = emptyList() + fun fluidBindings(): List = emptyList() + fun clear() {} +} + +private object NullTextureBinding : CParticleTextureBinding { + override fun sampleTexture(x: Double, y: Double, z: Double, out: FloatArray) {} +} + +private object NullFluidBinding : CParticleFluidBinding { + override fun sampleFluid(x: Double, y: Double, z: Double, out: FloatArray) {} +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt new file mode 100644 index 00000000..3b9643d4 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +class CParticleTextureResource(id: String) : CParticleForceResource(id), CParticleTextureBinding { + override fun sampleTexture(x: Double, y: Double, z: Double, out: FloatArray) {} + fun bindCompute(index: Int) {} + fun resetCompute() {} +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/simulate/CParticleGpuSimulator.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/simulate/CParticleGpuSimulator.kt index 21684359..b6e99d3d 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/simulate/CParticleGpuSimulator.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/simulate/CParticleGpuSimulator.kt @@ -5,6 +5,8 @@ import cn.coostack.cooparticlesapi.cparticle.CParticleCapabilities import cn.coostack.cooparticlesapi.cparticle.collision.CParticleBlockCollisionGrid import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceResourceTable import cn.coostack.cooparticlesapi.cparticle.force.CParticleForce +import cn.coostack.cooparticlesapi.cparticle.force.CParticleTextureResource +import cn.coostack.cooparticlesapi.cparticle.force.CParticleFluidResource import cn.coostack.cooparticlesapi.cparticle.force.ForceCommand import cn.coostack.cooparticlesapi.renderer.shader.AdvancedShaderProgramBuilder import cn.coostack.cooparticlesapi.renderer.shader.ShaderProgramRegistry @@ -180,8 +182,8 @@ object CParticleGpuSimulator { "[cparticle] ${if (useLegacy) "legacy Force" else "Force Command"} GPU compute program 无效,拒绝回退 CPU" } - val textureBindings = if (useLegacy) emptyList() else system.forceResourceTable.textureBindings() - val fluidBindings = if (useLegacy) emptyList() else system.forceResourceTable.fluidBindings() + val textureBindings = if (useLegacy) emptyList() else system.forceResourceTable.textureBindings() + val fluidBindings = if (useLegacy) emptyList() else system.forceResourceTable.fluidBindings() var dispatched = false try { compute.useOnContext { diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt new file mode 100644 index 00000000..93abf822 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt @@ -0,0 +1,10 @@ +package cn.coostack.cooparticlesapi.cparticle.storage + +class CParticleCommandGlBuffer { + val initialized: Boolean get() = false + fun init() {} + fun upload(data: FloatArray, count: Int) {} + fun release() {} + fun dispose() {} + fun bindShaderStorage(binding: Int) {} +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt index 3b1307cf..664d1d91 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt @@ -3,16 +3,16 @@ package cn.coostack.cooparticlesapi.data.holder import cn.coostack.cooparticlesapi.CooParticlesConstants import cn.coostack.cooparticlesapi.annotations.CooAutoRegister import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper +import cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec import cn.coostack.cooparticlesapi.network.packet.server.PacketDataHolderS2C import cn.coostack.cooparticlesapi.reflect.CooAPIScanner import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo -import net.minecraft.network.codec.StreamCodec import net.minecraft.world.entity.Entity import java.util.concurrent.ConcurrentHashMap object DataHolderManager { val entities = ConcurrentHashMap() - private val registeredTypes = ConcurrentHashMap>() + private val registeredTypes = ConcurrentHashMap>() fun getOrCreate(entity: Entity): DataHolder { return entities.getOrPut(entity) { DataHolder(entity) } @@ -33,11 +33,11 @@ object DataHolderManager { registeredTypes[randomInstance::class.java.name] = codec } - fun register(type: Class<*>, codec: StreamCodec<*, *>) { + fun register(type: Class<*>, codec: CommonStreamCodec<*>) { registeredTypes[type.name] = codec } - fun getCodecFromID(id: String): StreamCodec<*, *>? { + fun getCodecFromID(id: String): CommonStreamCodec<*>? { return registeredTypes[id] ?: CodecHelper.supposedTypes[id] } @@ -63,7 +63,7 @@ object DataHolderManager { register(instance) } - private fun findCodec(instance: Any): StreamCodec<*, *> { + private fun findCodec(instance: Any): CommonStreamCodec<*> { val codec = CodecHelper.supposedTypes[instance::class.java.name] return codec ?: throw IllegalStateException("DataHolder codec not registered for type: ${instance::class.java.name}") diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CommonCodec.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CommonCodec.kt new file mode 100644 index 00000000..cbf8fed8 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CommonCodec.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +interface CommonCodec { + fun encode(buf: PacketByteBuf, value: T) + fun decode(buf: PacketByteBuf): T + companion object { + fun of(enc: (PacketByteBuf, T) -> Unit, dec: (PacketByteBuf) -> T): CommonCodec = + object : CommonCodec { + override fun encode(buf: PacketByteBuf, value: T) = enc(buf, value) + override fun decode(buf: PacketByteBuf): T = dec(buf) + } + } +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt index e03b4951..0356f58d 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt @@ -12,7 +12,6 @@ import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelope import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkEndpoint import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkMetrics import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.network.protocol.common.custom.CustomPacketPayload import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import java.util.UUID @@ -332,7 +331,7 @@ object CooServerPacketManager { CooParticlesConstants.logger.error("CooPacket 编码失败: ${packet::class.java.name}", e) return false } - val envelope: CustomPacketPayload = CooPacketEnvelopeS2C( + val envelope = CooPacketEnvelopeS2C( kindId = kind.id, packetId = packet.id(), correlationId = correlationId, diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/PacketByteBuf.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/PacketByteBuf.kt new file mode 100644 index 00000000..56e905ae --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/PacketByteBuf.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +interface PacketByteBuf { + fun writeUtf(value: String) + fun writeResourceLocation(value: Any?) + fun writeInt(value: Int) + fun writeFloat(value: Float) + fun writeDouble(value: Double) + fun writeLong(value: Long) + fun writeBoolean(value: Boolean) + fun readUtf(): String + fun readResourceLocation(): Any? + fun readInt(): Int + fun readFloat(): Float + fun readDouble(): Double + fun readLong(): Long + fun readBoolean(): Boolean +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt index 33d43249..8764b96d 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt @@ -1,13 +1,5 @@ package cn.coostack.cooparticlesapi.platform -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.level.ChunkPos - interface ClientNetworking { - - fun send(packet: CustomPacketPayload) - - + fun send(packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket) } \ No newline at end of file diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt index 28ddc2e0..039afdf7 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt @@ -1,19 +1,15 @@ package cn.coostack.cooparticlesapi.platform -import net.minecraft.network.protocol.common.custom.CustomPacketPayload import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.level.ChunkPos interface ServerNetworking { - fun send(packet: CustomPacketPayload, to: ServerPlayer) - fun sendAllPlayers(packet: CustomPacketPayload) - fun sendToPlayersTrackingChunk(world: ServerLevel, chunk: ChunkPos, packet: CustomPacketPayload) + fun send(packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket, to: ServerPlayer) + fun sendAllPlayers(packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket) + fun sendToPlayersTrackingChunk(world: ServerLevel, chunk: ChunkPos, packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket) - /** - * 把数据包发送给指定世界 (维度) 内的所有玩家 - */ - fun sendToWorld(world: ServerLevel, packet: CustomPacketPayload) { + fun sendToWorld(world: ServerLevel, packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket) { world.players().forEach { send(packet, it) } } } \ No newline at end of file diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ServerContext.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ServerContext.kt index 8c219493..6a80bd1c 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ServerContext.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ServerContext.kt @@ -1,11 +1,10 @@ package cn.coostack.cooparticlesapi.platform.network -import net.minecraft.network.protocol.common.custom.CustomPacketPayload import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.player.Player interface ServerContext { fun player(): Player fun server(): MinecraftServer - fun reply(packet: CustomPacketPayload) + fun reply(packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket) } \ No newline at end of file diff --git a/forge/build.gradle b/forge/build.gradle new file mode 100644 index 00000000..c943e675 --- /dev/null +++ b/forge/build.gradle @@ -0,0 +1,99 @@ +buildscript { + repositories { + maven { + name = "Forge" + url = uri("https://maven.minecraftforge.net") + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath "net.minecraftforge.gradle:ForgeGradle:5.1.50" + } +} + +plugins { + id 'multiloader-loader' +} + +apply plugin: 'net.minecraftforge.gradle' + +tasks.javadoc { + enabled = false +} + +tasks.named('processResources', ProcessResources) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +minecraft { + version '1.20.1' + mappings channel: 'parchment', version: '1.20.1-2024.07.07' + runs { + client { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + } + server { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + } + data { + workingDirectory project.file('run') + args '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + } +} + +tasks.register("sourceWithCommon", Jar) { + def other = project(":common").tasks.findByName("sourcesJar") + if (other != null) { + dependsOn(other) + from zipTree(other.archiveFile) + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + destinationDirectory.set(file("../builds/sources")) + archiveFileName = "${rootProject.mod_name}-Forge-Source-${rootProject.version}.jar" +} + +tasks.named('jar', Jar) { + destinationDirectory.set(file("../builds/jar")) + archiveFileName.set("${rootProject.mod_name}-Forge-${rootProject.minecraft_version}-${rootProject.version}.jar") + dependsOn(tasks.named("sourceWithCommon")) +} + +repositories { + maven { + name = "Kotlin for Forge" + url = "https://thedarkcolour.github.io/KotlinForForge/" + } + maven { + name = "jsdu-snapshot" + url = "https://nexus.jsdu.cn/snapshots" + mavenContent { + snapshotsOnly() + } + } + maven { + name = "jsdu" + url = "https://nexus.jsdu.cn/repository" + } + flatDir { + dirs "../depend" + } +} + +dependencies { + minecraft "com.mojang:minecraft:1.20.1" + compileOnly fileTree(dir: "../compile_only_depend", includes: ["*.jar"]) + compileOnly "maven.modrinth:sodium:mc1.20.1-0.5.8-forge" + implementation "thedarkcolour:kotlinforforge-forge:5.10.0" + implementation "net.minecraftforge:forge:1.20.1-46.0.14" + runtimeOnly fileTree(dir: "../depend", includes: ["*.jar"]) +} + +sourceSets.main.resources { + srcDir 'src/generated/resources' +} diff --git a/forge/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java b/forge/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java new file mode 100644 index 00000000..3acd507e --- /dev/null +++ b/forge/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.mixin.compat.iris; + +public interface FinalPassRendererAccessor { + void coParticlesAPI$setMainTarget(net.minecraft.client.renderer.MultiBufferSource.BufferSource target); + net.minecraft.client.renderer.MultiBufferSource.BufferSource coParticlesAPI$getMainTarget(); +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/CooParticlesAPIForge.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/CooParticlesAPIForge.kt new file mode 100644 index 00000000..e99846b0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/CooParticlesAPIForge.kt @@ -0,0 +1,59 @@ +package cn.coostack.cooparticlesapi + +import cn.coostack.cooparticlesapi.entities.CooModEntityTypes +import cn.coostack.cooparticlesapi.items.CooItemForge +import cn.coostack.cooparticlesapi.items.group.CooItemGroup +import cn.coostack.cooparticlesapi.network.packet.api.CooClientPacketManager +import cn.coostack.cooparticlesapi.network.packet.api.CooServerPacketManager +import cn.coostack.cooparticlesapi.particles.CooModParticles +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraftforge.eventbus.api.SubscribeEvent +import net.minecraftforge.fml.common.Mod +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent +import net.minecraftforge.registries.RegisterEvent +import thedarkcolour.kotlinforforge.forge.MOD_BUS + +@Mod(CooParticlesConstants.MOD_ID) +object CooParticlesAPIForge { + init { + MOD_BUS.addListener(::onCommon) + MOD_BUS.addListener(::onRegistryRegister) + CooParticlesConstants.logger.info("Listener registered on CooParticlesForge Initialize") + CooParticlesAPI.init() + CooItemForge.reg(MOD_BUS) + CooItemGroup.reg() + CooParticlesServices.COO_REGISTRY.init(MOD_BUS) + setupNetwork() + } + + fun setupNetwork() { + cn.coostack.cooparticlesapi.platform.ForgeNetworkChannel.registerEnvelopeS2C { envelope -> + CooClientPacketManager.handleS2C(envelope) + } + cn.coostack.cooparticlesapi.platform.ForgeNetworkChannel.registerEnvelopeC2S { envelope, sender -> + CooServerPacketManager.handleC2S(envelope, sender) + } + } + + fun onCommon(event: FMLCommonSetupEvent) { + CooParticlesConstants.logger.info("所有模组加载完毕 CooParticlesAPI->Called test") + CooParticlesAPI.loadScannerPackages() + CooAPIScanner.neoLoaded() + } + + @SubscribeEvent + fun onRegistryRegister(event: RegisterEvent) { + event.register(BuiltInRegistries.PARTICLE_TYPE.key()) { + CooModParticles.particleTypes.forEach { type -> + it.register(type.id, type.get()) + } + } + event.register(BuiltInRegistries.ENTITY_TYPE.key()) { + CooModEntityTypes.types.forEach { type -> + it.register(type.id, type.get()) + } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt new file mode 100644 index 00000000..57657d27 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt @@ -0,0 +1,678 @@ +package cn.coostack.cooparticlesapi.annotations.codec + +import cn.coostack.cooparticlesapi.CodecHelperJava +import cn.coostack.cooparticlesapi.animation.timeline.ValueConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.ValueConstTimeAnimator +import cn.coostack.cooparticlesapi.animation.timeline.DoubleConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.DoubleConstTimeAnimator +import cn.coostack.cooparticlesapi.animation.timeline.FloatConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.FloatConstTimeAnimator +import cn.coostack.cooparticlesapi.animation.timeline.IntConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.IntConstTimeAnimator +import cn.coostack.cooparticlesapi.animation.timeline.RelativeLocationConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.RelativeLocationConstTimeAnimator +import cn.coostack.cooparticlesapi.animation.timeline.Vec3ConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.Vec3ConstTimeAnimator +import cn.coostack.cooparticlesapi.animation.timeline.Vector3fConstSpeedAnimator +import cn.coostack.cooparticlesapi.animation.timeline.Vector3fConstTimeAnimator +import cn.coostack.cooparticlesapi.barrages.HitBox +import cn.coostack.cooparticlesapi.cparticle.CParticleTextureSource +import cn.coostack.cooparticlesapi.cparticle.CParticleColorCurve +import cn.coostack.cooparticlesapi.cparticle.CParticleCurve +import cn.coostack.cooparticlesapi.cparticle.CParticleUpdateMode +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableCParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.CompositionEmittersData +import cn.coostack.cooparticlesapi.network.particle.emitters.DisplayEntityEmittersData +import cn.coostack.cooparticlesapi.network.particle.emitters.SimpleRandomParticleData +import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue +import cn.coostack.cooparticlesapi.network.particle.data.DoubleRangeData +import cn.coostack.cooparticlesapi.network.particle.data.FloatRangeData +import cn.coostack.cooparticlesapi.network.particle.data.IntRangeData +import cn.coostack.cooparticlesapi.utils.interpolator.data.InterpolatorDouble +import cn.coostack.cooparticlesapi.utils.interpolator.data.InterpolatorFloat +import cn.coostack.cooparticlesapi.utils.interpolator.data.InterpolatorVec3d +import cn.coostack.cooparticlesapi.utils.interpolator.data.InterpolatorVector3f +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.interpolator.data.InterpolatorRelativeLocation +import com.mojang.serialization.Codec +import net.minecraft.core.BlockPos +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec2 +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector3f +import org.joml.Vector4f +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.WildcardType +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +object CodecHelper { + val supposedTypes = ConcurrentHashMap>() + + /** + * 记录只能由 [RegistryFriendlyByteBuf] 驱动的 codec 类型。 + * + * Example: emitter 的 `ControlableCParticleData` 字段会通过 [registryCodecOf] 查询。 + * Forbidden: 普通 packet 或 RenderEntity codec 不能把这些类型当成 [FriendlyByteBuf] codec。 + */ + private val registryRequiredTypes = ConcurrentHashMap.newKeySet() + + init { + CodecHelperJava.init() + register(Short::class.java, CommonStreamCodec.of({ buf, i -> buf.writeShort(i.toInt()) }, { it.readShort() })) + register(Int::class.java, CommonStreamCodec.of({ buf, i -> buf.writeInt(i) }, { it.readInt() })) + register(Long::class.java, CommonStreamCodec.of({ buf, i -> buf.writeLong(i) }, { it.readLong() })) + register(LongArray::class.java, CommonStreamCodec.of({ buf, i -> buf.writeLongArray(i) }, { it.readLongArray() })) + register(Float::class.java, CommonStreamCodec.of({ buf, i -> buf.writeFloat(i) }, { it.readFloat() })) + register(Double::class.java, CommonStreamCodec.of({ buf, i -> buf.writeDouble(i) }, { it.readDouble() })) + register(String::class.java, CommonStreamCodec.of({ buf, i -> buf.writeUtf(i) }, { it.readUtf() })) + register(Byte::class.java, CommonStreamCodec.of({ buf, i -> buf.writeByte(i.toInt()) }, { it.readByte() })) + register(Boolean::class.java, CommonStreamCodec.of({ buf, i -> buf.writeBoolean(i) }, { it.readBoolean() })) + register(ByteArray::class.java, CommonStreamCodec.of({ buf, i -> buf.writeByteArray(i) }, { it.readByteArray() })) + register(CooUniformValue::class.java, CooUniformValue.STREAM_CODEC) + register(Char::class.java, CommonStreamCodec.of({ buf, i -> buf.writeChar(i.code) }, { it.readChar() })) + register(UUID::class.java, CommonStreamCodec.of({ buf, i -> buf.writeUUID(i) }, { it.readUUID() })) + registerRegistry(ControlableParticleData::class.java, ControlableParticleData.PACKET_CODEC) + registerRegistry(ControlableCParticleData::class.java, ControlableCParticleData.PACKET_CODEC) + registerRegistry(CParticleTextureSource::class.java, CParticleTextureSource.STREAM_CODEC) + register(CParticleCurve::class.java, CParticleCurve.STREAM_CODEC) + register(CParticleColorCurve::class.java, CParticleColorCurve.STREAM_CODEC) + register( + CParticleUpdateMode::class.java, + CommonStreamCodec.of( + { buf, mode -> buf.writeByte(mode.ordinal) }, + { buf -> + val ordinal = buf.readUnsignedByte().toInt() + require(ordinal < CParticleUpdateMode.entries.size) { + "unknown CParticle update mode: $ordinal" + } + CParticleUpdateMode.entries[ordinal] + }, + ), + ) + registerRegistry(CompositionEmittersData::class.java, CompositionEmittersData.PACKET_CODEC) + registerRegistry(DisplayEntityEmittersData::class.java, DisplayEntityEmittersData.PACKET_CODEC) + register(Vector3f::class.java, CommonStreamCodec.of({ buf, i -> buf.writeVector3f(i) }, { it.readVector3f() })) + register(Vector4f::class.java, CommonStreamCodec.of({ buf, v -> + buf.writeFloat(v.x) + buf.writeFloat(v.y) + buf.writeFloat(v.z) + buf.writeFloat(v.w) + }, { + Vector4f(it.readFloat(), it.readFloat(), it.readFloat(), it.readFloat()) + })) + register(Vec2::class.java, CommonStreamCodec.of({ buf, i -> + buf.writeFloat(i.x) + buf.writeFloat(i.y) + }, { + Vec2(it.readFloat(), it.readFloat()) + })) + register(Vec3::class.java, CommonStreamCodec.of({ buf, i -> buf.writeVec3(i) }, { it.readVec3() })) + register(Quaternionf::class.java, CommonStreamCodec.of({ buf, q -> buf.writeQuaternion(q) }, { it.readQuaternion() })) + register(AABB::class.java, CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.minX) + buf.writeDouble(i.minY) + buf.writeDouble(i.minZ) + buf.writeDouble(i.maxX) + buf.writeDouble(i.maxY) + buf.writeDouble(i.maxZ) + }, { + AABB(it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble()) + })) + register(HitBox::class.java, CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.x1) + buf.writeDouble(i.y1) + buf.writeDouble(i.z1) + buf.writeDouble(i.x2) + buf.writeDouble(i.y2) + buf.writeDouble(i.z2) + }, { + HitBox(it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble()) + })) + registerRegistry(ItemStack::class.java, ItemStack.OPTIONAL_STREAM_CODEC) + register(SimpleRandomParticleData::class.java, SimpleRandomParticleData.PACKET_CODEC) + register(RelativeLocation::class.java, CommonStreamCodec.of({ buf, r -> + buf.apply { + writeDouble(r.x) + writeDouble(r.y) + writeDouble(r.z) + } + }, { buf -> + RelativeLocation(buf.readDouble(), buf.readDouble(), buf.readDouble()) + })) + register(InterpolatorDouble::class.java, InterpolatorDouble.CODEC) + register(InterpolatorFloat::class.java, InterpolatorFloat.CODEC) + register(InterpolatorVec3d::class.java, InterpolatorVec3d.CODEC) + register(InterpolatorVector3f::class.java, InterpolatorVector3f.CODEC) + register(InterpolatorRelativeLocation::class.java, InterpolatorRelativeLocation.CODEC) + register( + DoubleRangeData::class.java, + CommonStreamCodec.of({ buf, i -> buf.writeDouble(i.min); buf.writeDouble(i.max) }, { + DoubleRangeData(it.readDouble(), it.readDouble()) + }) + ) + register( + IntRangeData::class.java, + CommonStreamCodec.of({ buf, i -> buf.writeInt(i.min); buf.writeInt(i.max) }, { + IntRangeData(it.readInt(), it.readInt()) + }) + ) + register( + FloatRangeData::class.java, + CommonStreamCodec.of({ buf, i -> buf.writeFloat(i.min); buf.writeFloat(i.max) }, { + FloatRangeData(it.readFloat(), it.readFloat()) + }) + ) + register( + BlockPos::class.java, + CommonStreamCodec.of({ buf, pos -> buf.writeBlockPos(pos) }, { buf -> buf.readBlockPos() }) + ) + register( + BlockState::class.java, + CommonStreamCodec.of({ buf, s -> + val id = net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.getId(s) + buf.writeVarInt(id) + }, { buf -> + val id = buf.readVarInt() + net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) + ?: net.minecraft.world.level.block.Blocks.AIR.defaultBlockState() + }) + ) + register( + ValueConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeDouble(i.targetNum.toDouble()) + buf.writeDouble(i.current.toDouble()) + }, { + ValueConstTimeAnimator(it.readInt(), it.readDouble()) + .resetCurrentTo(it.readDouble()) + }) + ) + register( + ValueConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.speed.toDouble()) + buf.writeDouble(i.targetNum.toDouble()) + buf.writeDouble(i.current.toDouble()) + }, { + ValueConstSpeedAnimator(it.readDouble(), it.readDouble()) + .resetCurrentTo(it.readDouble()) + }) + ) + register( + DoubleConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeDouble(i.targetNum) + buf.writeDouble(i.current) + }, { + DoubleConstTimeAnimator(it.readInt(), it.readDouble()) + .resetCurrentTo(it.readDouble()) + }) + ) + register( + FloatConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeFloat(i.targetNum) + buf.writeFloat(i.current) + }, { + FloatConstTimeAnimator(it.readInt(), it.readFloat()) + .resetCurrentTo(it.readFloat()) + }) + ) + register( + IntConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeInt(i.targetNum) + buf.writeDouble(i.currentRaw) + }, { + IntConstTimeAnimator(it.readInt(), it.readInt()) + .resetCurrentRawTo(it.readDouble()) + }) + ) + register( + Vec3ConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeVec3(i.targetNum) + buf.writeVec3(i.current) + }, { + Vec3ConstTimeAnimator(it.readInt(), it.readVec3()) + .resetCurrentTo(it.readVec3()) + }) + ) + register( + RelativeLocationConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeDouble(i.targetNum.x) + buf.writeDouble(i.targetNum.y) + buf.writeDouble(i.targetNum.z) + buf.writeDouble(i.current.x) + buf.writeDouble(i.current.y) + buf.writeDouble(i.current.z) + }, { + RelativeLocationConstTimeAnimator( + it.readInt(), + RelativeLocation(it.readDouble(), it.readDouble(), it.readDouble()) + ).resetCurrentTo(RelativeLocation(it.readDouble(), it.readDouble(), it.readDouble())) + }) + ) + register( + Vector3fConstTimeAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.durationTick) + buf.writeVector3f(i.targetNum) + buf.writeVector3f(i.current) + }, { + Vector3fConstTimeAnimator(it.readInt(), it.readVector3f()) + .resetCurrentTo(it.readVector3f()) + }) + ) + register( + DoubleConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.speed) + buf.writeDouble(i.targetNum) + buf.writeDouble(i.current) + }, { + DoubleConstSpeedAnimator(it.readDouble(), it.readDouble()) + .resetCurrentTo(it.readDouble()) + }) + ) + register( + FloatConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeFloat(i.speed) + buf.writeFloat(i.targetNum) + buf.writeFloat(i.current) + }, { + FloatConstSpeedAnimator(it.readFloat(), it.readFloat()) + .resetCurrentTo(it.readFloat()) + }) + ) + register( + IntConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeInt(i.speed) + buf.writeInt(i.targetNum) + buf.writeDouble(i.currentRaw) + }, { + IntConstSpeedAnimator(it.readInt(), it.readInt()) + .resetCurrentRawTo(it.readDouble()) + }) + ) + register( + Vec3ConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.speed) + buf.writeVec3(i.targetNum) + buf.writeVec3(i.current) + }, { + Vec3ConstSpeedAnimator(it.readDouble(), it.readVec3()) + .resetCurrentTo(it.readVec3()) + }) + ) + register( + RelativeLocationConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.speed) + buf.writeDouble(i.targetNum.x) + buf.writeDouble(i.targetNum.y) + buf.writeDouble(i.targetNum.z) + buf.writeDouble(i.current.x) + buf.writeDouble(i.current.y) + buf.writeDouble(i.current.z) + }, { + RelativeLocationConstSpeedAnimator( + it.readDouble(), + RelativeLocation(it.readDouble(), it.readDouble(), it.readDouble()) + ).resetCurrentTo(RelativeLocation(it.readDouble(), it.readDouble(), it.readDouble())) + }) + ) + register( + Vector3fConstSpeedAnimator::class.java, + CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.speed) + buf.writeVector3f(i.targetNum) + buf.writeVector3f(i.current) + }, { + Vector3fConstSpeedAnimator(it.readDouble(), it.readVector3f()) + .resetCurrentTo(it.readVector3f()) + }) + ) + } + + /** + * 编解码方式注册器 + * + * 可以直接向 codec的 ByteBuf里写入 (大概就是writeInt这些) + * + * 示例: + * ```kotlin + * StreamCodec.of( + * { buf,option-> + * // 这里假设option有2个参数 一个id: String 一个age:Int + * buf.writeUtf(option.id) + * buf.writeInt(option.age) + * },{ + * CustomOption(it.readUtf(),it.readInt()) + * } + * ) + * ``` + * + * @param T 要编码的类型 + * @param type 类型对应的类 + * @param codec 他的编解码器 + */ + @JvmStatic + fun register(type: Class, codec: CommonStreamCodec) { + supposedTypes[type.name] = codec + registryRequiredTypes.remove(type.name) + } + + /** + * 注册依赖注册表上下文的字段 codec。 + * + * Example: `ControlableCParticleData.PACKET_CODEC` 由 emitter 自动 codec 使用。 + * Forbidden: 不要把只调用基础 `writeInt` 等操作的普通 codec 注册到这里。 + * + * @param T 要编码的类型 + * @param type 类型对应的类 + * @param codec 需要 [RegistryFriendlyByteBuf] 的 codec + */ + @JvmStatic + fun registerRegistry( + type: Class, + codec: CommonStreamCodec, + ) { + supposedTypes[type.name] = codec + registryRequiredTypes.add(type.name) + } + + /** + * 转换为该list 基于该泛型的codec + * + * @param type + */ + + fun codecOf(type: Type): CommonStreamCodec<*> { + val codecType = normalizeCodecType(type) + + if (codecType is Class<*>) { + require(codecType.name !in registryRequiredTypes) { + "类型 ${codecType.name} 需要 RegistryFriendlyByteBuf;请使用 registryCodecOf" + } + return supposedTypes[codecType.name] + ?: throw IllegalArgumentException("不支持的类型: ${codecType.name}") + } + + if (codecType is ParameterizedType) { + val raw = codecType.rawType as Class<*> + + if (List::class.java.isAssignableFrom(raw)) { + return codecList(codecType) + } + + if (Set::class.java.isAssignableFrom(raw)) { + return codecSet(codecType) + } + + if (Map::class.java.isAssignableFrom(raw)) { + return codecMap(codecType) + } + } + + throw IllegalArgumentException("不支持的字段类型: $type") + } + + /** + * 返回可在注册表网络上下文中使用的字段 codec。 + * + * 普通 [FriendlyByteBuf] codec 也可安全用于其子类 [RegistryFriendlyByteBuf];集合会递归保持该约束。 + * Example: emitter 的 `@CodecField var template = ControlableCParticleData()` 使用本入口。 + * Forbidden: 调用方不能把返回值降级后传入普通 [FriendlyByteBuf]。 + * + * @param type 字段的反射类型 + * @return 接受 [RegistryFriendlyByteBuf] 的字段 codec + */ + @Suppress("UNCHECKED_CAST") + fun registryCodecOf(type: Type): CommonStreamCodec<*> { + val codecType = normalizeCodecType(type) + + if (codecType is Class<*>) { + return supposedTypes[codecType.name] as? CommonStreamCodec<*> + ?: throw IllegalArgumentException("不支持的类型: ${codecType.name}") + } + + if (codecType is ParameterizedType) { + val raw = codecType.rawType as Class<*> + if (List::class.java.isAssignableFrom(raw)) return registryCodecList(codecType) + if (Set::class.java.isAssignableFrom(raw)) return registryCodecSet(codecType) + if (Map::class.java.isAssignableFrom(raw)) return registryCodecMap(codecType) + } + + throw IllegalArgumentException("不支持的字段类型: $type") + } + + private fun normalizeCodecType(type: Type): Type { + if (type is WildcardType) { + if (type.lowerBounds.isNotEmpty()) { + throw IllegalArgumentException("不支持的字段类型: $type") + } + return type.upperBounds.firstOrNull() ?: Any::class.java + } + if (type is Class<*>) { + return when (type) { + java.lang.Short::class.java -> Short::class.java + java.lang.Integer::class.java -> Int::class.java + java.lang.Long::class.java -> Long::class.java + java.lang.Float::class.java -> Float::class.java + java.lang.Double::class.java -> Double::class.java + java.lang.Byte::class.java -> Byte::class.java + java.lang.Boolean::class.java -> Boolean::class.java + java.lang.Character::class.java -> Char::class.java + else -> type + } + } + return type + } + + @Suppress("UNCHECKED_CAST") + fun codecList(type: Type): CommonStreamCodec<*> { + if (type !is ParameterizedType) { + throw IllegalArgumentException("List字段必须声明具体泛型: $type") + } + + val elementType = type.actualTypeArguments[0] + val elementCodec = codecOf(elementType) as CommonStreamCodec + + return CommonStreamCodec.of({ buf, value -> + { buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("List字段不支持null元素: $type")) + } + }, + { buf -> + val size = buf.readVarInt() + val list = ArrayList(size) + repeat(size) { + list.add(elementCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState()) + } + list + } + ) + } + + @Suppress("UNCHECKED_CAST") + fun codecSet(type: Type): CommonStreamCodec<*> { + if (type !is ParameterizedType) { + throw IllegalArgumentException("Set字段必须声明具体泛型: $type") + } + + val elementType = type.actualTypeArguments[0] + val elementCodec = codecOf(elementType) as CommonStreamCodec + + return CommonStreamCodec.of({ buf, value -> + { buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("Set字段不支持null元素: $type")) + } + }, + { buf -> + val size = buf.readVarInt() + val set = LinkedHashSet(size) + repeat(size) { + set.add(elementCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState()) + } + set + } + ) + } + + @Suppress("UNCHECKED_CAST") + fun codecMap(type: Type): CommonStreamCodec<*> { + if (type !is ParameterizedType) { + throw IllegalArgumentException("Map字段必须声明具体泛型: $type") + } + + val keyType = type.actualTypeArguments[0] + val valueType = type.actualTypeArguments[1] + val keyCodec = codecOf(keyType) as CommonStreamCodec + val valueCodec = codecOf(valueType) as CommonStreamCodec + + return CommonStreamCodec.of({ buf, value -> + { buf, value -> + buf.writeVarInt(value.size) + value.forEach { (key, mapValue) -> + keyCodec.encode(buf, key ?: error("Map字段不支持null键: $type")) + valueCodec.encode(buf, mapValue ?: error("Map字段不支持null值: $type")) + } + }, + { buf -> + val size = buf.readVarInt() + val map = LinkedHashMap(size) + repeat(size) { + map[keyCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState()] = valueCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState() + } + map + } + ) + } + + /** + * 创建 registry-aware 的 List 字段 codec。 + * + * Example: emitter 可声明 `List`。 + * Forbidden: List 必须声明具体元素类型,且不支持 `null` 元素。 + * + * @param type 带具体元素类型的 List 反射类型 + * @return registry-aware List codec + */ + @Suppress("UNCHECKED_CAST") + private fun registryCodecList(type: ParameterizedType): CommonStreamCodec<*> { + val elementCodec = registryCodecOf(type.actualTypeArguments[0]) as + CommonStreamCodec + return CommonStreamCodec.of({ buf, value -> + { buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("List字段不支持null元素: $type")) + } + }, + { buf -> + val size = buf.readVarInt() + List(size) { elementCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState() } + }, + ) + } + + /** + * 创建 registry-aware 的 Set 字段 codec。 + * + * Example: emitter 可声明 `Set`。 + * Forbidden: Set 必须声明具体元素类型,且不支持 `null` 元素。 + * + * @param type 带具体元素类型的 Set 反射类型 + * @return registry-aware Set codec + */ + @Suppress("UNCHECKED_CAST") + private fun registryCodecSet(type: ParameterizedType): CommonStreamCodec<*> { + val elementCodec = registryCodecOf(type.actualTypeArguments[0]) as + CommonStreamCodec + return CommonStreamCodec.of({ buf, value -> + { buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("Set字段不支持null元素: $type")) + } + }, + { buf -> + val size = buf.readVarInt() + LinkedHashSet(size).apply { + repeat(size) { add(elementCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState()) } + } + }, + ) + } + + /** + * 创建 registry-aware 的 Map 字段 codec。 + * + * Example: emitter 可声明 `Map`。 + * Forbidden: Map 必须声明具体键值类型,且不支持 `null` 键或值。 + * + * @param type 带具体键值类型的 Map 反射类型 + * @return registry-aware Map codec + */ + @Suppress("UNCHECKED_CAST") + private fun registryCodecMap(type: ParameterizedType): CommonStreamCodec<*> { + val keyCodec = registryCodecOf(type.actualTypeArguments[0]) as + CommonStreamCodec + val valueCodec = registryCodecOf(type.actualTypeArguments[1]) as + CommonStreamCodec + return CommonStreamCodec.of({ buf, value -> + { buf, value -> + buf.writeVarInt(value.size) + value.forEach { (key, mapValue) -> + keyCodec.encode(buf, key ?: error("Map字段不支持null键: $type")) + valueCodec.encode(buf, mapValue ?: error("Map字段不支持null值: $type")) + } + }, + { buf -> + val size = buf.readVarInt() + LinkedHashMap(size).apply { + repeat(size) { put(keyCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState(), valueCodecval id = buf.readVarInt(); BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: Blocks.AIR.defaultBlockState()) } + } + }, + ) + } + + + fun updateFields(current: Any, other: Any) { + if (current::class.java != other::class.java) return + CodecFieldAccessor.fields(current::class.java).forEach { field -> + CodecFieldAccessor.set(field, current, CodecFieldAccessor.get(field, other)) + } + } + + + fun isSupposedType(type: Class<*>) = supposedTypes[type.name] != null + + fun isSupposedType(type: String) = supposedTypes[type] != null + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeCodecHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeCodecHelper.kt new file mode 100644 index 00000000..3559ac52 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeCodecHelper.kt @@ -0,0 +1,306 @@ +package cn.coostack.cooparticlesapi.annotations.codec + +import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.core.particles.ParticleType +import net.minecraft.core.particles.ParticleTypes +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec2 +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector2f +import org.joml.Vector3f +import org.joml.Vector4f +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +object ForgeCodecHelper { + val supposedTypes = ConcurrentHashMap>() + private val registryRequiredTypes = ConcurrentHashMap.newKeySet() + + init { + register(Short::class.java, CommonStreamCodec.of({ buf, i -> buf.writeShort(i.toInt()) }, { it.readShort() })) + register(Int::class.java, CommonStreamCodec.of({ buf, i -> buf.writeInt(i) }, { it.readInt() })) + register(Long::class.java, CommonStreamCodec.of({ buf, i -> buf.writeLong(i) }, { it.readLong() })) + register(LongArray::class.java, CommonStreamCodec.of({ buf, i -> buf.writeLongArray(i) }, { it.readLongArray() })) + register(Float::class.java, CommonStreamCodec.of({ buf, i -> buf.writeFloat(i) }, { it.readFloat() })) + register(Double::class.java, CommonStreamCodec.of({ buf, i -> buf.writeDouble(i) }, { it.readDouble() })) + register(String::class.java, CommonStreamCodec.of({ buf, i -> buf.writeUtf(i) }, { it.readUtf() })) + register(Byte::class.java, CommonStreamCodec.of({ buf, i -> buf.writeByte(i.toInt()) }, { it.readByte() })) + register(Boolean::class.java, CommonStreamCodec.of({ buf, i -> buf.writeBoolean(i) }, { it.readBoolean() })) + register(ByteArray::class.java, CommonStreamCodec.of({ buf, i -> buf.writeByteArray(i) }, { it.readByteArray() })) + register(CooUniformValue::class.java, CooUniformValue.STREAM_CODEC) + register(Char::class.java, CommonStreamCodec.of({ buf, i -> buf.writeChar(i.code) }, { it.readChar() })) + register(UUID::class.java, CommonStreamCodec.of({ buf, i -> buf.writeUUID(i) }, { it.readUUID() })) + register( + BlockPos::class.java, + CommonStreamCodec.of({ buf, pos -> buf.writeBlockPos(pos) }, { buf -> buf.readBlockPos() }) + ) + register( + BlockState::class.java, + CommonStreamCodec.of({ buf, state -> + val id = net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.getId(state) + buf.writeVarInt(id) + }, { buf -> + val id = buf.readVarInt() + net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) + ?: net.minecraft.world.level.block.Blocks.AIR.defaultBlockState() + }) + ) + register(Vector3f::class.java, CommonStreamCodec.of({ buf, i -> buf.writeVector3f(i) }, { it.readVector3f() })) + register(Vector4f::class.java, CommonStreamCodec.of({ buf, v -> + buf.writeFloat(v.x) + buf.writeFloat(v.y) + buf.writeFloat(v.z) + buf.writeFloat(v.w) + }, { + Vector4f(it.readFloat(), it.readFloat(), it.readFloat(), it.readFloat()) + })) + register(Vec2::class.java, CommonStreamCodec.of({ buf, i -> + buf.writeFloat(i.x) + buf.writeFloat(i.y) + }, { + Vec2(it.readFloat(), it.readFloat()) + })) + register(Vec3::class.java, CommonStreamCodec.of({ buf, i -> buf.writeVec3(i) }, { it.readVec3() })) + register(Quaternionf::class.java, CommonStreamCodec.of({ buf, q -> buf.writeQuaternion(q) }, { it.readQuaternion() })) + register(AABB::class.java, CommonStreamCodec.of({ buf, i -> + buf.writeDouble(i.minX) + buf.writeDouble(i.minY) + buf.writeDouble(i.minZ) + buf.writeDouble(i.maxX) + buf.writeDouble(i.maxY) + buf.writeDouble(i.maxZ) + }, { + AABB(it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble(), it.readDouble()) + })) + register(ItemStack::class.java, CommonStreamCodec.of({ buf, i -> buf.writeItem(i) }, { it.readItem() })) + } + + @JvmStatic + fun register(type: Class, codec: CommonStreamCodec) { + supposedTypes[type.name] = codec + registryRequiredTypes.remove(type.name) + } + + @JvmStatic + fun registerRegistry(type: Class, codec: CommonStreamCodec) { + supposedTypes[type.name] = codec + registryRequiredTypes.add(type.name) + } + + fun codecOf(type: Type): CommonStreamCodec<*> { + val codecType = normalizeCodecType(type) + if (codecType is Class<*>) { + require(codecType.name !in registryRequiredTypes) { + "Type ${codecType.name} requires registry-aware codec; use registryCodecOf" + } + return supposedTypes[codecType.name] + ?: throw IllegalArgumentException("Unsupported type: ${codecType.name}") + } + if (codecType is ParameterizedType) { + val raw = codecType.rawType as Class<*> + if (List::class.java.isAssignableFrom(raw)) return codecList(codecType) + if (Set::class.java.isAssignableFrom(raw)) return codecSet(codecType) + if (Map::class.java.isAssignableFrom(raw)) return codecMap(codecType) + } + throw IllegalArgumentException("Unsupported field type: $type") + } + + @Suppress("UNCHECKED_CAST") + fun registryCodecOf(type: Type): CommonStreamCodec<*> { + val codecType = normalizeCodecType(type) + if (codecType is Class<*>) { + return supposedTypes[codecType.name] as? CommonStreamCodec<*> + ?: throw IllegalArgumentException("Unsupported type: ${codecType.name}") + } + if (codecType is ParameterizedType) { + val raw = codecType.rawType as Class<*> + if (List::class.java.isAssignableFrom(raw)) return registryCodecList(codecType) + if (Set::class.java.isAssignableFrom(raw)) return registryCodecSet(codecType) + if (Map::class.java.isAssignableFrom(raw)) return registryCodecMap(codecType) + } + throw IllegalArgumentException("Unsupported field type: $type") + } + + private fun normalizeCodecType(type: Type): Type { + if (type is WildcardType) { + if (type.lowerBounds.isNotEmpty()) { + throw IllegalArgumentException("Unsupported field type: $type") + } + return type.upperBounds.firstOrNull() ?: Any::class.java + } + if (type is Class<*>) { + return when (type) { + java.lang.Short::class.java -> Short::class.java + java.lang.Integer::class.java -> Int::class.java + java.lang.Long::class.java -> Long::class.java + java.lang.Float::class.java -> Float::class.java + java.lang.Double::class.java -> Double::class.java + java.lang.Byte::class.java -> Byte::class.java + java.lang.Boolean::class.java -> Boolean::class.java + java.lang.Character::class.java -> Char::class.java + else -> type + } + } + return type + } + + @Suppress("UNCHECKED_CAST") + fun codecList(type: Type): CommonStreamCodec<*> { + if (type !is ParameterizedType) { + throw IllegalArgumentException("List field must declare generic type: $type") + } + val elementType = type.actualTypeArguments[0] + val elementCodec = codecOf(elementType) + return CommonStreamCodec.of({ buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("List field does not support null elements: $type")) + } + }, { buf -> + val size = buf.readVarInt() + val list = ArrayList(size) + repeat(size) { + list.add(elementCodec.decode(buf)) + } + list + }) + } + + @Suppress("UNCHECKED_CAST") + fun codecSet(type: Type): CommonStreamCodec<*> { + if (type !is ParameterizedType) { + throw IllegalArgumentException("Set field must declare generic type: $type") + } + val elementType = type.actualTypeArguments[0] + val elementCodec = codecOf(elementType) + return CommonStreamCodec.of({ buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("Set field does not support null elements: $type")) + } + }, { buf -> + val size = buf.readVarInt() + val set = LinkedHashSet(size) + repeat(size) { + set.add(elementCodec.decode(buf)) + } + set + }) + } + + @Suppress("UNCHECKED_CAST") + fun codecMap(type: Type): CommonStreamCodec<*> { + if (type !is ParameterizedType) { + throw IllegalArgumentException("Map field must declare generic type: $type") + } + val keyType = type.actualTypeArguments[0] + val valueType = type.actualTypeArguments[1] + val keyCodec = codecOf(keyType) + val valueCodec = codecOf(valueType) + return CommonStreamCodec.of({ buf, value -> + buf.writeVarInt(value.size) + value.forEach { (key, mapValue) -> + keyCodec.encode(buf, key ?: error("Map field does not support null keys: $type")) + valueCodec.encode(buf, mapValue ?: error("Map field does not support null values: $type")) + } + }, { buf -> + val size = buf.readVarInt() + val map = LinkedHashMap(size) + repeat(size) { + map[keyCodec.decode(buf)] = valueCodec.decode(buf) + } + map + }) + } + + @Suppress("UNCHECKED_CAST") + private fun registryCodecList(type: ParameterizedType): CommonStreamCodec<*> { + val elementCodec = registryCodecOf(type.actualTypeArguments[0]) + return CommonStreamCodec.of({ buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("List field does not support null elements: $type")) + } + }, { buf -> + val size = buf.readVarInt() + List(size) { elementCodec.decode(buf) } + }) + } + + @Suppress("UNCHECKED_CAST") + private fun registryCodecSet(type: ParameterizedType): CommonStreamCodec<*> { + val elementCodec = registryCodecOf(type.actualTypeArguments[0]) + return CommonStreamCodec.of({ buf, value -> + buf.writeVarInt(value.size) + value.forEach { element -> + elementCodec.encode(buf, element ?: error("Set field does not support null elements: $type")) + } + }, { buf -> + val size = buf.readVarInt() + LinkedHashSet(size).apply { + repeat(size) { add(elementCodec.decode(buf)) } + } + }) + } + + @Suppress("UNCHECKED_CAST") + private fun registryCodecMap(type: ParameterizedType): CommonStreamCodec<*> { + val keyCodec = registryCodecOf(type.actualTypeArguments[0]) + val valueCodec = registryCodecOf(type.actualTypeArguments[1]) + return CommonStreamCodec.of({ buf, value -> + buf.writeVarInt(value.size) + value.forEach { (key, mapValue) -> + keyCodec.encode(buf, key ?: error("Map field does not support null keys: $type")) + valueCodec.encode(buf, mapValue ?: error("Map field does not support null values: $type")) + } + }, { buf -> + val size = buf.readVarInt() + LinkedHashMap(size).apply { + repeat(size) { put(keyCodec.decode(buf), valueCodec.decode(buf)) } + } + }) + } + + fun updateFields(current: Any, other: Any) { + if (current::class.java != other::class.java) return + CodecFieldAccessor.fields(current::class.java).forEach { field -> + CodecFieldAccessor.set(field, current, CodecFieldAccessor.get(field, other)) + } + } + + fun isSupposedType(type: Class<*>) = supposedTypes[type.name] != null + fun isSupposedType(type: String) = supposedTypes[type] != null + + @JvmStatic + fun particleCodecOf(particle: ParticleOptions): CommonStreamCodec { + return CommonStreamCodec.of({ buf, p -> + val id = BuiltInRegistries.PARTICLE_TYPE.getKey(p.type) + buf.writeResourceLocation(id) + if (p is cn.coostack.cooparticlesapi.particles.ControlableParticleEffect) { + val codec = p.getPacketCodec() + codec.encode(buf, p) + } else { + p.writeToPacket(buf) + } + }, { buf -> + val id = buf.readResourceLocation() + val type = BuiltInRegistries.PARTICLE_TYPE.get(id) + ?: error("Unknown particle type: $id") + @Suppress("UNCHECKED_CAST") + val particleType = type as ParticleType + val result = particleType.codec.parse( + BuiltInRegistries.PARTICLE_TYPE.asSerializerId(), + net.minecraft.server.packs.resources.ResourceManager.Empty() + ) + result.orElseThrow() + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeStreamCodec.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeStreamCodec.kt new file mode 100644 index 00000000..381b0c61 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeStreamCodec.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.annotations.codec + +interface ForgeStreamCodec { + fun decode(buf: net.minecraft.network.PacketByteBuf): T + fun encode(buf: net.minecraft.network.PacketByteBuf, value: T) + companion object { + fun of(dec: (net.minecraft.network.PacketByteBuf) -> T, enc: (net.minecraft.network.PacketByteBuf, T) -> Unit): ForgeStreamCodec = + object : ForgeStreamCodec { + override fun decode(buf: net.minecraft.network.PacketByteBuf): T = dec(buf) + override fun encode(buf: net.minecraft.network.PacketByteBuf, value: T) = enc(buf, value) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/composition/handler/ParticleCompositionRegistryHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/composition/handler/ParticleCompositionRegistryHelper.kt new file mode 100644 index 00000000..de72c334 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/composition/handler/ParticleCompositionRegistryHelper.kt @@ -0,0 +1,73 @@ +package cn.coostack.cooparticlesapi.annotations.composition.handler + +import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper +import cn.coostack.cooparticlesapi.annotations.codec.CodecFieldAccessor +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.network.particle.composition.SequencedParticleComposition +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +object ParticleCompositionRegistryHelper { + fun generateCodec(randomInstance: ParticleComposition): CommonStreamCodec { + return generateCodec(randomInstance::class.java) + } + + fun generateCodec(type: Class): CommonStreamCodec { + val pw = runCatching { type.getConstructor(Vec3::class.java, Level::class.java) }.getOrNull() + val wp = if (pw == null) runCatching { + type.getConstructor( + Level::class.java, + Vec3::class.java + ) + }.getOrNull() else null + val p = + if (pw == null && wp == null) runCatching { type.getConstructor(Vec3::class.java) }.getOrNull() else null + val w = + if (pw == null && wp == null && p == null) runCatching { type.getConstructor(Level::class.java) }.getOrNull() else null + val empty = + if (pw == null && wp == null && w == null && p == null) type.getConstructor() else null + return CommonStreamCodec.of( + { buf, composition -> + if (composition is SequencedParticleComposition) { + SequencedParticleComposition.encodeBase(composition, buf) + } else { + ParticleComposition.encodeBase(composition, buf) + } + val fields = CodecFieldAccessor.fields(type) + fields.forEach { + it.isAccessible = true + @Suppress("UNCHECKED_CAST") + val codec: CommonStreamCodec = + CodecHelper.codecOf(CodecFieldAccessor.valueType(it)) as CommonStreamCodec + codec.encode(buf, CodecFieldAccessor.get(it, composition)) + } + }, { buf -> + val instance = when { + pw != null -> pw.newInstance(Vec3.ZERO, null) + wp != null -> wp.newInstance(null, Vec3.ZERO) + p != null -> p.newInstance(Vec3.ZERO) + w != null -> w.newInstance(null) + empty != null -> empty.newInstance() + else -> throw NullPointerException("All constructors failed") + } + instance.apply { + if (this is SequencedParticleComposition) { + SequencedParticleComposition.decodeBase(this, buf) + } else { + ParticleComposition.decodeBase(this, buf) + } + val fields = CodecFieldAccessor.fields(type) + fields.forEach { + it.isAccessible = true + @Suppress("UNCHECKED_CAST") + val codec: CommonStreamCodec = + CodecHelper.codecOf(CodecFieldAccessor.valueType(it)) as CommonStreamCodec + val value = codec.decode(buf) + CodecFieldAccessor.set(it, this, value) + } + } + } + ) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/display/handle/DisplayEntityRegistryHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/display/handle/DisplayEntityRegistryHelper.kt new file mode 100644 index 00000000..19860c45 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/display/handle/DisplayEntityRegistryHelper.kt @@ -0,0 +1,47 @@ +package cn.coostack.cooparticlesapi.annotations.display.handle + +import cn.coostack.cooparticlesapi.annotations.codec.CodecFieldAccessor +import cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec +import cn.coostack.cooparticlesapi.annotations.codec.ForgeCodecHelper +import cn.coostack.cooparticlesapi.display.DisplayEntity +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +object DisplayEntityRegistryHelper { + + fun generateCodec(randomInstance: DisplayEntity): CommonStreamCodec { + val type = randomInstance::class.java + val constructor = type.getConstructor(Vec3::class.java, Level::class.java) + return CommonStreamCodec.of( + { buf, display -> + display as DisplayEntity + DisplayEntity.encodeBase(display, buf) + val fields = CodecFieldAccessor.fields(type) + + fields.forEach { + it.isAccessible = true + @Suppress("UNCHECKED_CAST") + val codec: CommonStreamCodec = + ForgeCodecHelper.registryCodecOf(CodecFieldAccessor.valueType(it)) as CommonStreamCodec + codec.encode(buf, CodecFieldAccessor.get(it, display)) + } + }, { buf -> + constructor.newInstance(Vec3.ZERO, null).apply { + DisplayEntity.decodeBase(this, buf) + val fields = CodecFieldAccessor.fields(type) + + fields.forEach { + it.isAccessible = true + @Suppress("UNCHECKED_CAST") + val codec: CommonStreamCodec = + ForgeCodecHelper.registryCodecOf(CodecFieldAccessor.valueType(it)) as CommonStreamCodec + + val value = codec.decode(buf) + CodecFieldAccessor.set(it, this, value) + } + } + } + ) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/emitter/handle/ParticleEmittersRegistryHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/emitter/handle/ParticleEmittersRegistryHelper.kt new file mode 100644 index 00000000..a76379cd --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/emitter/handle/ParticleEmittersRegistryHelper.kt @@ -0,0 +1,111 @@ +package cn.coostack.cooparticlesapi.annotations.emitter.handle + +import cn.coostack.cooparticlesapi.annotations.codec.CodecFieldAccessor +import cn.coostack.cooparticlesapi.annotations.codec.ForgeCodecHelper +import cn.coostack.cooparticlesapi.network.particle.emitters.ClassEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.TransformableCParticleEmitter +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +object ParticleEmittersRegistryHelper { + fun updateEmitter(current: ClassParticleEmitters, other: ClassParticleEmitters) { + if (current.getEmittersID() != other.getEmittersID()) return + ForgeCodecHelper.updateFields(current, other) + } + + fun updateEmitter(current: ClassEmitters, other: ClassEmitters) { + if (current.getEmittersID() != other.getEmittersID()) return + ForgeCodecHelper.updateFields(current, other) + } + + fun updateEmitter(current: TransformableCParticleEmitter, other: TransformableCParticleEmitter) { + if (current.getEmittersID() != other.getEmittersID()) return + ForgeCodecHelper.updateFields(current, other) + } + + fun generateCodec(randomInstance: ClassParticleEmitters): CommonStreamCodec { + return generateClassParticleCodec(randomInstance::class.java) + } + + fun generateClassParticleCodec(type: Class): CommonStreamCodec { + val constructor = type.getConstructor(Vec3::class.java, Level::class.java) + return CommonStreamCodec.of( + { buf, emitter -> + emitter as ClassParticleEmitters + ClassParticleEmitters.encodeBase(emitter, buf) + encodeFields(type, emitter, buf) + }, + { buf -> + constructor.newInstance(Vec3.ZERO, null).apply { + ClassParticleEmitters.decodeBase(this, buf) + decodeFields(type, this, buf) + } + } + ) + } + + fun generateCodec(randomInstance: TransformableCParticleEmitter): CommonStreamCodec { + return generateTransformableCParticleEmitterCodec(randomInstance::class.java) + } + + fun generateTransformableCParticleEmitterCodec( + type: Class, + ): CommonStreamCodec { + val constructor = type.getConstructor(Vec3::class.java, Level::class.java) + return CommonStreamCodec.of( + { buf, emitter -> + emitter as TransformableCParticleEmitter + TransformableCParticleEmitter.encodeBase(emitter, buf) + encodeFields(type, emitter, buf) + }, + { buf -> + constructor.newInstance(Vec3.ZERO, null).apply { + TransformableCParticleEmitter.decodeBase(this, buf) + decodeFields(type, this, buf) + } + }, + ) + } + + fun generateCodec(randomInstance: ClassEmitters): CommonStreamCodec { + return generateClassEmittersCodec(randomInstance::class.java) + } + + fun generateClassEmittersCodec(type: Class): CommonStreamCodec { + val constructor = type.getConstructor(Vec3::class.java, Level::class.java) + return CommonStreamCodec.of( + { buf, emitter -> + emitter as ClassEmitters + ClassEmitters.encodeBase(emitter, buf) + encodeFields(type, emitter, buf) + }, + { buf -> + constructor.newInstance(Vec3.ZERO, null).apply { + ClassEmitters.decodeBase(this, buf) + decodeFields(type, this, buf) + } + } + ) + } + + @Suppress("UNCHECKED_CAST") + private fun encodeFields(type: Class<*>, emitter: Any, buf: PacketByteBuf) { + CodecFieldAccessor.fields(type).forEach { field -> + val codec = ForgeCodecHelper.registryCodecOf(CodecFieldAccessor.valueType(field)) as + CommonStreamCodec + codec.encode(buf, CodecFieldAccessor.get(field, emitter)) + } + } + + @Suppress("UNCHECKED_CAST") + private fun decodeFields(type: Class<*>, emitter: Any, buf: PacketByteBuf) { + CodecFieldAccessor.fields(type).forEach { field -> + val codec = ForgeCodecHelper.registryCodecOf(CodecFieldAccessor.valueType(field)) as + CommonStreamCodec + CodecFieldAccessor.set(field, emitter, codec.decode(buf)) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistry.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistry.kt new file mode 100644 index 00000000..e7e641cf --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistry.kt @@ -0,0 +1,95 @@ +package cn.coostack.cooparticlesapi.annotations.packet + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import io.netty.buffer.Unpooled +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.concurrent.ConcurrentHashMap + +object CooPacketRegistry { + private data class Entry( + val packetClass: Class, + val codec: CommonStreamCodec, + ) + + private val byId = ConcurrentHashMap() + private val byClass = ConcurrentHashMap, ResourceLocation>() + + private var scanned = false + + fun registerScanner() { + if (scanned) return + scanned = true + val start = System.currentTimeMillis() + val infos = CooAPIScanner.getWithAnnotation(CooAutoRegister::class.java) + var registered = 0 + infos.forEach { info -> + val clazz = runCatching { info.toClass() }.getOrNull() ?: return@forEach + if (!CooPacket::class.java.isAssignableFrom(clazz)) return@forEach + @Suppress("UNCHECKED_CAST") + val packetClass = clazz as Class + try { + register(packetClass) + registered++ + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket auto-register failed: ${packetClass.name}", e + ) + } + } + val end = System.currentTimeMillis() + CooParticlesConstants.logger.info("CooPacket auto-register complete: $registered packets, took ${end - start}ms") + } + + fun register(packetClass: Class) { + val sample = try { + packetClass.getDeclaredConstructor().apply { isAccessible = true }.newInstance() + } catch (e: NoSuchMethodException) { + throw IllegalStateException("CooPacket ${packetClass.name} must have a no-arg constructor", e) + } + val id = sample.id() + @Suppress("UNCHECKED_CAST") + val codec = sample.codec() as CommonStreamCodec + val existing = byId[id] + if (existing != null && existing.packetClass != packetClass) { + throw IllegalStateException("CooPacket ID conflict: $id is used by both ${existing.packetClass.name} and ${packetClass.name}") + } + byId[id] = Entry(packetClass, codec) + byClass[packetClass] = id + } + + fun isRegistered(id: ResourceLocation): Boolean = byId.containsKey(id) + fun isRegistered(packetClass: Class): Boolean = byClass.containsKey(packetClass) + + fun idOf(packet: CooPacket): ResourceLocation { + return byClass[packet::class.java] ?: packet.id() + } + + fun encode(packet: CooPacket): ByteArray { + val entry = byId[packet.id()] + ?: throw IllegalStateException( + "CooPacket not registered: ${packet::class.java.name} (id=${packet.id()}). " + + "Make sure the class has @CooAutoRegister and is in the scan package" + ) + val buf = PacketByteBuf(Unpooled.buffer()) + entry.codec.encode(buf, packet) + val bytes = ByteArray(buf.readableBytes()) + buf.readBytes(bytes) + buf.release() + return bytes + } + + fun decode(id: ResourceLocation, data: ByteArray): CooPacket? { + val entry = byId[id] ?: return null + val buf = PacketByteBuf(Unpooled.wrappedBuffer(data)) + return try { + entry.codec.decode(buf) + } catch (e: Throwable) { + CooParticlesConstants.logger.error("CooPacket decode failed: $id", e) + null + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistryHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistryHelper.kt new file mode 100644 index 00000000..4e9f8314 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistryHelper.kt @@ -0,0 +1,44 @@ +package cn.coostack.cooparticlesapi.annotations.packet + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import io.netty.buffer.Unpooled +import net.minecraft.network.PacketByteBuf +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +object CooPacketRegistryHelper { + fun generateClassParticleCodec(type: Class): CommonStreamCodec { + val constructor = type.getConstructor() + return CommonStreamCodec.of( + { buf, packet -> + packet as CooPacket + val fields = codecFields(type) + fields.forEach { field -> + field.isAccessible = true + val codec = CodecHelper.codecOf(field.genericType) + codec.encode(buf, field.get(packet)) + } + }, + { buf -> + constructor.newInstance().apply { + val fields = codecFields(type) + fields.forEach { field -> + field.isAccessible = true + val codec = CodecHelper.codecOf(field.genericType) + val value = codec.decode(buf) + field.set(this, value) + } + } + } + ) + } + + private fun codecFields(type: Class<*>): List { + return type.declaredFields + .filter { it.isAnnotationPresent(CodecField::class.java) && !Modifier.isFinal(it.modifiers) } + .sortedBy { it.name } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/renderer/handle/RenderEntityRegistryHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/renderer/handle/RenderEntityRegistryHelper.kt new file mode 100644 index 00000000..8f34420c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/renderer/handle/RenderEntityRegistryHelper.kt @@ -0,0 +1,71 @@ +package cn.coostack.cooparticlesapi.annotations.renderer.handle + +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.codec.CodecFieldAccessor +import cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec +import cn.coostack.cooparticlesapi.annotations.codec.ForgeCodecHelper +import cn.coostack.cooparticlesapi.renderer.RenderEntity +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.lang.reflect.Modifier + +object RenderEntityRegistryHelper { + + fun generateCodec(randomInstance: RenderEntity): CommonStreamCodec { + val type = randomInstance::class.java + val noArgCtor = runCatching { type.getConstructor() }.getOrNull() + val levelVecCtor = runCatching { type.getConstructor(Level::class.java, Vec3::class.java) }.getOrNull() + val factory = when { + noArgCtor != null -> { + { noArgCtor.newInstance() as RenderEntity } + } + levelVecCtor != null -> { + { levelVecCtor.newInstance(null, Vec3.ZERO) as RenderEntity } + } + else -> { + throw IllegalStateException( + "RenderEntity requires public no-arg or (Level, Vec3) constructor: ${type.name}" + ) + } + } + + return CommonStreamCodec.of( + { buf, entity -> + RenderEntity.encodeBase(buf, entity) + val fields = type.declaredFields.filter { + it.isAnnotationPresent(CodecField::class.java) && + !Modifier.isFinal(it.modifiers) && + !Modifier.isStatic(it.modifiers) + }.sortedBy { it.name } + + fields.forEach { field -> + field.isAccessible = true + @Suppress("UNCHECKED_CAST") + val codec: CommonStreamCodec = + ForgeCodecHelper.codecOf(field.genericType) as CommonStreamCodec + codec.encode(buf, field.get(entity)) + } + }, + { buf -> + factory().apply { + RenderEntity.decodeBase(buf, this) + val fields = type.declaredFields.filter { + it.isAnnotationPresent(CodecField::class.java) && + !Modifier.isFinal(it.modifiers) && + !Modifier.isStatic(it.modifiers) + }.sortedBy { it.name } + + fields.forEach { field -> + field.isAccessible = true + @Suppress("UNCHECKED_CAST") + val codec: CommonStreamCodec = + ForgeCodecHelper.codecOf(field.genericType) as CommonStreamCodec + val value = codec.decode(buf) + field.set(this, value) + } + } + } + ) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt new file mode 100644 index 00000000..fadb635d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt @@ -0,0 +1,21 @@ +package cn.coostack.cooparticlesapi.api.controler + +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.phys.Vec3 + +interface SerializableData { + fun getCodec(): ForgeStreamCodec + + fun clone(): SerializableData + + fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> + + fun getDisplayer(): ParticleDisplayer +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt new file mode 100644 index 00000000..6f3d99e0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt @@ -0,0 +1,256 @@ +package cn.coostack.cooparticlesapi.cparticle + +import net.minecraft.network.PacketByteBuf + +import org.joml.Vector3f +import org.joml.Vector3fc + +class CParticleColorCurve private constructor( + packedTimes: FloatArray, + packedColors: FloatArray, + val keyCount: Int, + val interpolation: CParticleCurveInterpolation, + packedOutHandles: FloatArray, + packedInHandles: FloatArray, +) { + internal val packedTimeData: FloatArray = packedTimes + internal val packedColorData: FloatArray = packedColors + internal val packedOutHandleData: FloatArray = packedOutHandles + internal val packedInHandleData: FloatArray = packedInHandles + + val packedTimes: FloatArray + get() = packedTimeData.copyOf() + val packedColors: FloatArray + get() = packedColorData.copyOf() + val packedOutHandles: FloatArray + get() = packedOutHandleData.copyOf() + val packedInHandles: FloatArray + get() = packedInHandleData.copyOf() + + internal fun sample(t: Float, destination: Vector3f): Vector3f { + val sampleT = t.coerceIn(0f, 1f) + if (sampleT <= packedTimeData[0]) return colorAt(0, destination) + for (i in 1 until keyCount) { + if (sampleT <= packedTimeData[i]) { + val t0 = packedTimeData[i - 1] + val t1 = packedTimeData[i] + val fromOffset = (i - 1) * COLOR_COMPONENTS + val toOffset = i * COLOR_COMPONENTS + if (interpolation == CParticleCurveInterpolation.CUBIC_BEZIER) { + val previousHandle = (i - 1) * HANDLE_COMPONENTS + val currentHandle = i * HANDLE_COMPONENTS + val parameter = CParticleBezierMath.parameterAt( + sampleT, + t0, + packedOutHandleData[previousHandle], + t1, + packedInHandleData[currentHandle], + ) + return destination.set( + sampleBezierColor(parameter, fromOffset, toOffset, previousHandle, currentHandle, 0), + sampleBezierColor(parameter, fromOffset, toOffset, previousHandle, currentHandle, 1), + sampleBezierColor(parameter, fromOffset, toOffset, previousHandle, currentHandle, 2), + ) + } + val progress = if (t1 > t0) (sampleT - t0) / (t1 - t0) else 0f + return destination.set( + lerp(packedColorData[fromOffset], packedColorData[toOffset], progress), + lerp(packedColorData[fromOffset + 1], packedColorData[toOffset + 1], progress), + lerp(packedColorData[fromOffset + 2], packedColorData[toOffset + 2], progress), + ) + } + } + return colorAt(keyCount - 1, destination) + } + + private fun sampleBezierColor( + parameter: Float, + fromOffset: Int, + toOffset: Int, + previousHandle: Int, + currentHandle: Int, + component: Int, + ): Float { + val from = packedColorData[fromOffset + component] + val to = packedColorData[toOffset + component] + return CParticleBezierMath.cubic( + parameter, + from, + from + packedOutHandleData[previousHandle + 1 + component], + to + packedInHandleData[currentHandle + 1 + component], + to, + ) + } + + private fun colorAt(index: Int, destination: Vector3f): Vector3f { + val offset = index * COLOR_COMPONENTS + return destination.set( + packedColorData[offset], + packedColorData[offset + 1], + packedColorData[offset + 2], + ) + } + + companion object { + const val MAX_KEYS = CParticleCurve.MAX_KEYS + private const val COLOR_COMPONENTS = 3 + internal const val HANDLE_COMPONENTS = 4 + private const val EXTENDED_CODEC_MARKER = 0 + + @JvmField + val STREAM_CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, curve -> + if (curve.interpolation == CParticleCurveInterpolation.LINEAR) { + buf.writeByte(curve.keyCount) + for (i in 0 until curve.keyCount) { + val colorOffset = i * COLOR_COMPONENTS + buf.writeFloat(curve.packedTimeData[i]) + buf.writeFloat(curve.packedColorData[colorOffset]) + buf.writeFloat(curve.packedColorData[colorOffset + 1]) + buf.writeFloat(curve.packedColorData[colorOffset + 2]) + } + } else { + buf.writeByte(EXTENDED_CODEC_MARKER) + buf.writeByte(curve.interpolation.wireId) + buf.writeByte(curve.keyCount) + for (i in 0 until curve.keyCount) { + val colorOffset = i * COLOR_COMPONENTS + val handleOffset = i * HANDLE_COMPONENTS + buf.writeFloat(curve.packedTimeData[i]) + repeat(COLOR_COMPONENTS) { component -> + buf.writeFloat(curve.packedColorData[colorOffset + component]) + } + repeat(HANDLE_COMPONENTS) { component -> + buf.writeFloat(curve.packedOutHandleData[handleOffset + component]) + } + repeat(HANDLE_COMPONENTS) { component -> + buf.writeFloat(curve.packedInHandleData[handleOffset + component]) + } + } + } + }, + { buf -> + val markerOrCount = buf.readUnsignedByte().toInt() + if (markerOrCount != EXTENDED_CODEC_MARKER) { + require(markerOrCount in 1..MAX_KEYS) { + "color curve key count must be in 1..$MAX_KEYS: $markerOrCount" + } + of(*Array(markerOrCount) { + buf.readFloat() to Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()) + }) + } else { + val interpolation = CParticleCurveInterpolation.fromWireId(buf.readUnsignedByte().toInt()) + require(interpolation == CParticleCurveInterpolation.CUBIC_BEZIER) { + "extended color curve must use cubic Bezier interpolation" + } + val count = buf.readUnsignedByte().toInt() + require(count in 1..MAX_KEYS) { "color curve key count must be in 1..$MAX_KEYS: $count" } + bezier(*Array(count) { + CParticleBezierColorKeyframe( + time = buf.readFloat().toDouble(), + value = Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + outX = buf.readFloat().toDouble(), + outValueOffset = Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + inX = buf.readFloat().toDouble(), + inValueOffset = Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + ) + }) + } + }, + ) + + @JvmStatic + fun of(vararg keys: Pair): CParticleColorCurve { + require(keys.isNotEmpty()) { "color curve requires at least 1 key" } + val count = keys.size.coerceAtMost(MAX_KEYS) + val times = FloatArray(MAX_KEYS) + val colors = FloatArray(MAX_KEYS * COLOR_COMPONENTS) + var previousTime = Float.NEGATIVE_INFINITY + for (i in 0 until count) { + val (time, color) = keys[i] + require(time.isFinite() && time in 0f..1f) { + "color curve time must be finite and in 0..1" + } + require(time >= previousTime) { "color curve keys must be sorted by time" } + require(color.x().isFinite() && color.y().isFinite() && color.z().isFinite()) { + "color curve values must be finite" + } + times[i] = time + val offset = i * COLOR_COMPONENTS + colors[offset] = color.x() + colors[offset + 1] = color.y() + colors[offset + 2] = color.z() + previousTime = time + } + return CParticleColorCurve( + times, + colors, + count, + CParticleCurveInterpolation.LINEAR, + FloatArray(MAX_KEYS * HANDLE_COMPONENTS), + FloatArray(MAX_KEYS * HANDLE_COMPONENTS), + ) + } + + @JvmStatic + fun bezier(vararg keys: CParticleBezierColorKeyframe): CParticleColorCurve { + require(keys.size in 1..MAX_KEYS) { "Bezier color curve requires 1..$MAX_KEYS keys" } + val times = FloatArray(MAX_KEYS) + val colors = FloatArray(MAX_KEYS * COLOR_COMPONENTS) + val outHandles = FloatArray(MAX_KEYS * HANDLE_COMPONENTS) + val inHandles = FloatArray(MAX_KEYS * HANDLE_COMPONENTS) + keys.forEachIndexed { index, key -> + require(key.time.isFinite() && key.outX.isFinite() && key.inX.isFinite()) { + "Bezier color curve times and handles must be finite" + } + require(key.time in 0.0..1.0) { "Bezier color curve time must be in 0..1" } + require(key.value.isFinite() && key.outValueOffset.isFinite() && key.inValueOffset.isFinite()) { + "Bezier color curve values and handles must be finite" + } + val time = key.time.toFloat() + val outX = key.outX.toFloat() + val inX = key.inX.toFloat() + require(time.isFinite() && outX.isFinite() && inX.isFinite()) { + "Bezier color curve times and handles must fit finite GPU floats" + } + times[index] = time + val colorOffset = index * COLOR_COMPONENTS + colors[colorOffset] = key.value.x() + colors[colorOffset + 1] = key.value.y() + colors[colorOffset + 2] = key.value.z() + val handleOffset = index * HANDLE_COMPONENTS + outHandles[handleOffset] = outX + outHandles[handleOffset + 1] = key.outValueOffset.x() + outHandles[handleOffset + 2] = key.outValueOffset.y() + outHandles[handleOffset + 3] = key.outValueOffset.z() + inHandles[handleOffset] = inX + inHandles[handleOffset + 1] = key.inValueOffset.x() + inHandles[handleOffset + 2] = key.inValueOffset.y() + inHandles[handleOffset + 3] = key.inValueOffset.z() + if (index > 0) { + CParticleBezierMath.requireMonotonicSegment( + times[index - 1], + outHandles[handleOffset - HANDLE_COMPONENTS], + times[index], + inHandles[handleOffset], + ) + } + } + return CParticleColorCurve( + times, + colors, + keys.size, + CParticleCurveInterpolation.CUBIC_BEZIER, + outHandles, + inHandles, + ) + } + + @JvmStatic + fun linear(from: Vector3fc, to: Vector3fc): CParticleColorCurve = + of(0f to from, 1f to to) + + private fun lerp(from: Float, to: Float, progress: Float): Float = + from + (to - from) * progress + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleCurve.kt new file mode 100644 index 00000000..88b1c92c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleCurve.kt @@ -0,0 +1,201 @@ +package cn.coostack.cooparticlesapi.cparticle + +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.BezierFloatKeyframe +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.BezierKeyframeFloatCurve +import net.minecraft.network.PacketByteBuf + +class CParticleCurve private constructor( + packed: FloatArray, + val keyCount: Int, + val interpolation: CParticleCurveInterpolation, + packedHandles: FloatArray, +) { + internal val packedData: FloatArray = packed + internal val packedHandleData: FloatArray = packedHandles + + val packed: FloatArray + get() = packedData.copyOf() + val packedHandles: FloatArray + get() = packedHandleData.copyOf() + + internal fun sample(t: Float): Float { + val sampleT = t.coerceIn(0f, 1f) + if (sampleT <= packedData[0]) return packedData[MAX_KEYS] + for (i in 1 until keyCount) { + if (sampleT <= packedData[i]) { + val t0 = packedData[i - 1] + val t1 = packedData[i] + val from = packedData[MAX_KEYS + i - 1] + val to = packedData[MAX_KEYS + i] + if (interpolation == CParticleCurveInterpolation.LINEAR) { + val progress = if (t1 > t0) (sampleT - t0) / (t1 - t0) else 0f + return from + (to - from) * progress + } + val previousHandle = (i - 1) * CParticleBezierMath.SCALAR_HANDLE_COMPONENTS + val currentHandle = i * CParticleBezierMath.SCALAR_HANDLE_COMPONENTS + val parameter = CParticleBezierMath.parameterAt( + sampleT, + t0, + packedHandleData[previousHandle], + t1, + packedHandleData[currentHandle + 2], + ) + return CParticleBezierMath.cubic( + parameter, + from, + from + packedHandleData[previousHandle + 1], + to + packedHandleData[currentHandle + 3], + to, + ) + } + } + return packedData[MAX_KEYS + keyCount - 1] + } + + companion object { + const val MAX_KEYS = 8 + private const val EXTENDED_CODEC_MARKER = 0 + + @JvmField + val STREAM_CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, curve -> + if (curve.interpolation == CParticleCurveInterpolation.LINEAR) { + buf.writeByte(curve.keyCount) + for (i in 0 until curve.keyCount) { + buf.writeFloat(curve.packedData[i]) + buf.writeFloat(curve.packedData[MAX_KEYS + i]) + } + } else { + buf.writeByte(EXTENDED_CODEC_MARKER) + buf.writeByte(curve.interpolation.wireId) + buf.writeByte(curve.keyCount) + for (i in 0 until curve.keyCount) { + val handleOffset = i * CParticleBezierMath.SCALAR_HANDLE_COMPONENTS + buf.writeFloat(curve.packedData[i]) + buf.writeFloat(curve.packedData[MAX_KEYS + i]) + repeat(CParticleBezierMath.SCALAR_HANDLE_COMPONENTS) { component -> + buf.writeFloat(curve.packedHandleData[handleOffset + component]) + } + } + } + }, + { buf -> + val markerOrCount = buf.readUnsignedByte().toInt() + if (markerOrCount != EXTENDED_CODEC_MARKER) { + require(markerOrCount in 1..MAX_KEYS) { + "curve key count must be in 1..$MAX_KEYS: $markerOrCount" + } + of(*Array(markerOrCount) { buf.readFloat() to buf.readFloat() }) + } else { + val interpolation = CParticleCurveInterpolation.fromWireId(buf.readUnsignedByte().toInt()) + require(interpolation == CParticleCurveInterpolation.CUBIC_BEZIER) { + "extended scalar curve must use cubic Bezier interpolation" + } + val count = buf.readUnsignedByte().toInt() + require(count in 1..MAX_KEYS) { "curve key count must be in 1..$MAX_KEYS: $count" } + bezier(*Array(count) { + BezierFloatKeyframe( + time = buf.readFloat().toDouble(), + value = buf.readFloat().toDouble(), + outX = buf.readFloat().toDouble(), + outY = buf.readFloat().toDouble(), + inX = buf.readFloat().toDouble(), + inY = buf.readFloat().toDouble(), + ) + }) + } + }, + ) + + @JvmStatic + fun of(vararg keys: Pair): CParticleCurve { + require(keys.isNotEmpty()) { "curve requires at least 1 key" } + val count = keys.size.coerceAtMost(MAX_KEYS) + val packed = FloatArray(MAX_KEYS * 2) + var previousTime = Float.NEGATIVE_INFINITY + for (i in 0 until count) { + val (time, value) = keys[i] + require(time.isFinite() && time in 0f..1f) { + "curve time must be finite and in 0..1" + } + require(time >= previousTime) { "curve keys must be sorted by time" } + require(value.isFinite()) { "curve value must be finite" } + packed[i] = time + packed[MAX_KEYS + i] = value + previousTime = time + } + return CParticleCurve( + packed, + count, + CParticleCurveInterpolation.LINEAR, + FloatArray(MAX_KEYS * CParticleBezierMath.SCALAR_HANDLE_COMPONENTS), + ) + } + + @JvmStatic + fun bezier(vararg keys: BezierFloatKeyframe): CParticleCurve { + require(keys.size in 1..MAX_KEYS) { "Bezier curve requires 1..$MAX_KEYS keys" } + val packed = FloatArray(MAX_KEYS * 2) + val handles = FloatArray(MAX_KEYS * CParticleBezierMath.SCALAR_HANDLE_COMPONENTS) + keys.forEachIndexed { index, key -> + require( + key.time.isFinite() && key.value.isFinite() && key.outX.isFinite() && + key.outY.isFinite() && key.inX.isFinite() && key.inY.isFinite() + ) { "Bezier curve keys and handles must be finite" } + require(key.time in 0.0..1.0) { "Bezier curve time must be in 0..1" } + val time = key.time.toFloat() + val value = key.value.toFloat() + val handleOffset = index * CParticleBezierMath.SCALAR_HANDLE_COMPONENTS + val outX = key.outX.toFloat() + val outY = key.outY.toFloat() + val inX = key.inX.toFloat() + val inY = key.inY.toFloat() + require(time.isFinite() && value.isFinite() && outX.isFinite() && outY.isFinite() && + inX.isFinite() && inY.isFinite()) { + "Bezier curve keys and handles must fit finite GPU floats" + } + packed[index] = time + packed[MAX_KEYS + index] = value + handles[handleOffset] = outX + handles[handleOffset + 1] = outY + handles[handleOffset + 2] = inX + handles[handleOffset + 3] = inY + if (index > 0) { + val previousHandle = handleOffset - CParticleBezierMath.SCALAR_HANDLE_COMPONENTS + CParticleBezierMath.requireMonotonicSegment( + packed[index - 1], + handles[previousHandle], + packed[index], + handles[handleOffset + 2], + ) + } + } + return CParticleCurve(packed, keys.size, CParticleCurveInterpolation.CUBIC_BEZIER, handles) + } + + @JvmStatic + fun linear(from: Float, to: Float): CParticleCurve = of(0f to from, 1f to to) + + @JvmStatic + fun fadeInOut(peak: Float = 1f, fadeIn: Float = 0.15f, fadeOut: Float = 0.75f): CParticleCurve = + of(0f to 0f, fadeIn to peak, fadeOut to peak, 1f to 0f) + + @JvmStatic + fun fromFloatCurve(curve: FloatCurve): CParticleCurve { + if (curve is BezierKeyframeFloatCurve) { + try { + return bezier(*curve.frames().toTypedArray()) + } catch (_: IllegalArgumentException) { + } + } + return sampledLinear(curve) + } + + private fun sampledLinear(curve: FloatCurve): CParticleCurve = + of(*Array(MAX_KEYS) { index -> + val time = index / (MAX_KEYS - 1f) + time to curve.sample(time.toDouble()).toFloat() + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleTextureSource.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleTextureSource.kt new file mode 100644 index 00000000..0754fb2d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleTextureSource.kt @@ -0,0 +1,232 @@ +@file:JvmName("CParticleTextures") + +package cn.coostack.cooparticlesapi.cparticle + +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.core.particles.ParticleTypes +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.block.Block as MinecraftBlock +import net.minecraft.world.level.block.state.BlockState +import org.joml.Vector3f + +sealed interface CParticleTextureSource { + companion object { + const val MAX_ATLAS_ANIMATION_FRAMES: Int = 4096 + + val STREAM_CODEC: ForgeStreamCodec = ForgeStreamCodec.of(::encodeSource, ::decodeSource) + + private fun encodeSource( + buf: PacketByteBuf, + source: CParticleTextureSource, + ) { + when (source) { + is ParticleEffect -> { + buf.writeByte(0) + ForgeCodecHelper.particleCodecOf(source.effect).encode(buf, source.effect) + buf.writeBoolean(source.animateByAge) + } + is AtlasSprite -> { + buf.writeByte(1) + buf.writeResourceLocation(source.atlasLocation) + buf.writeResourceLocation(source.spriteLocation) + } + is AtlasAnimation -> { + require(source.spriteLocations.size <= MAX_ATLAS_ANIMATION_FRAMES) { + "CParticle atlas animation exceeds $MAX_ATLAS_ANIMATION_FRAMES frames" + } + buf.writeByte(2) + buf.writeResourceLocation(source.atlasLocation) + buf.writeVarInt(source.spriteLocations.size) + source.spriteLocations.forEach(buf::writeResourceLocation) + } + is Block -> { + buf.writeByte(3) + val id = net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.getId(source.state) + buf.writeVarInt(id) + buf.writeBoolean(source.randomCrop) + buf.writeBoolean(source.applyTint) + buf.writeBoolean(source.applyBrightness) + } + is Item -> { + buf.writeByte(4) + buf.writeItem(source.resolveStack()) + buf.writeInt(source.modelSeed) + buf.writeBoolean(source.applyTint) + buf.writeInt(source.tintIndex) + } + is Custom -> { + buf.writeByte(5) + buf.writeResourceLocation(source.textureLocation) + writeUv(buf, source.uv) + } + } + } + + private fun decodeSource(buf: PacketByteBuf): CParticleTextureSource { + return when (val type = buf.readUnsignedByte().toInt()) { + 0 -> ParticleEffect( + ForgeCodecHelper.particleCodecOf(ParticleTypes.END_ROD).decode(buf), + animateByAge = buf.readBoolean(), + ) + 1 -> AtlasSprite( + buf.readResourceLocation(), + buf.readResourceLocation(), + ) + 2 -> { + val atlas = buf.readResourceLocation() + val frameCount = buf.readVarInt() + require(frameCount in 1..MAX_ATLAS_ANIMATION_FRAMES) { + "CParticle atlas animation frame count is invalid: $frameCount" + } + AtlasAnimation( + atlas, + List(frameCount) { buf.readResourceLocation() }, + ) + } + 3 -> { + val id = buf.readVarInt() + val state = net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) + ?: net.minecraft.world.level.block.Blocks.AIR.defaultBlockState() + Block( + state, + randomCrop = buf.readBoolean(), + applyTint = buf.readBoolean(), + applyBrightness = buf.readBoolean(), + ) + } + 4 -> Item( + buf.readItem(), + modelSeed = buf.readInt(), + applyTint = buf.readBoolean(), + tintIndex = buf.readInt(), + ) + 5 -> Custom( + buf.readResourceLocation(), + readUv(buf), + ) + else -> throw IllegalArgumentException("Unknown CParticle texture source type: $type") + } + } + + private fun writeUv(buf: PacketByteBuf, uv: CParticleUv) { + buf.writeFloat(uv.u0) + buf.writeFloat(uv.v0) + buf.writeFloat(uv.u1) + buf.writeFloat(uv.v1) + } + + private fun readUv(buf: PacketByteBuf): CParticleUv = CParticleUv( + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + ) + } + + data class ParticleEffect( + val effect: ParticleOptions, + val animateByAge: Boolean = true, + ) : CParticleTextureSource + + data class AtlasSprite( + val atlasLocation: ResourceLocation, + val spriteLocation: ResourceLocation, + ) : CParticleTextureSource + + class AtlasAnimation( + val atlasLocation: ResourceLocation, + spriteLocations: List, + ) : CParticleTextureSource { + val spriteLocations: List = spriteLocations.toList() + + init { + require(this.spriteLocations.size in 1..MAX_ATLAS_ANIMATION_FRAMES) { + "Atlas animation frame count must be in 1..$MAX_ATLAS_ANIMATION_FRAMES" + } + } + } + + data class Block( + val state: BlockState, + val randomCrop: Boolean = true, + val applyTint: Boolean = true, + val applyBrightness: Boolean = true, + ) : CParticleTextureSource + + class Item internal constructor( + stack: ItemStack, + val modelSeed: Int = 0, + val applyTint: Boolean = true, + val tintIndex: Int = 0, + ) : CParticleTextureSource { + private val snapshot = stack.copy() + + fun stackCopy(): ItemStack = snapshot.copy() + internal fun resolveStack(): ItemStack = snapshot + } + + data class Custom( + val textureLocation: ResourceLocation, + val uv: CParticleUv = CParticleUv.FULL, + ) : CParticleTextureSource +} + +fun interface CParticleTextureSourceProvider { + fun cparticleTextureSource(): CParticleTextureSource +} + +data class CParticleResolvedTexture( + val bindingKey: CParticleTextureBindingKey, + val descriptorId: Int, + val uv: CParticleUv, + val animationId: Int?, + val colorMultiplier: Vector3f, +) { + val isValid: Boolean + get() = bindingKey != CParticleTextureBindingKey.MISSING +} + +@JvmOverloads +fun textureOfBlock( + state: BlockState, + randomCrop: Boolean = true, + applyTint: Boolean = true, + applyBrightness: Boolean = true, +): CParticleTextureSource = CParticleTextureSource.Block(state, randomCrop, applyTint, applyBrightness) + +@JvmOverloads +fun textureOfItem( + stack: ItemStack, + modelSeed: Int = 0, + applyTint: Boolean = true, + tintIndex: Int = 0, +): CParticleTextureSource = CParticleTextureSource.Item(stack, modelSeed, tintIndex) + +@JvmOverloads +fun textureOf( + textureLocation: ResourceLocation, + uv: CParticleUv = CParticleUv.FULL, +): CParticleTextureSource = CParticleTextureSource.Custom(textureLocation, uv) + +fun textureOfAtlas( + atlasLocation: ResourceLocation, + spriteLocation: ResourceLocation, +): CParticleTextureSource = CParticleTextureSource.AtlasSprite(atlasLocation, spriteLocation) + +fun textureAnimationOfAtlas( + atlasLocation: ResourceLocation, + spriteLocations: List, +): CParticleTextureSource = CParticleTextureSource.AtlasAnimation(atlasLocation, spriteLocations.toList()) + +fun textureOfEffect(effect: ParticleOptions): CParticleTextureSource = + (effect as? CParticleTextureSourceProvider)?.cparticleTextureSource() + ?: CParticleTextureSource.ParticleEffect(effect) + +fun textureOfParticleSprite(spriteLocation: ResourceLocation): CParticleTextureSource = + if (spriteLocation == CParticleSprites.DEFAULT) { + CParticleTextureSource.ParticleEffect(ParticleTypes.END_ROD, animateByAge = false) + } else { + CParticleTextureSource.AtlasSprite(TextureAtlas.LOCATION_PARTICLES, spriteLocation) + } diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt new file mode 100644 index 00000000..a26196b7 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt @@ -0,0 +1,80 @@ +package cn.coostack.cooparticlesapi.data.holder + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper +import cn.coostack.cooparticlesapi.network.packet.server.PacketDataHolderS2C +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo +import net.minecraft.world.entity.Entity +import java.util.concurrent.ConcurrentHashMap + +object DataHolderManager { + val entities = ConcurrentHashMap() + private val registeredTypes = ConcurrentHashMap>() + + fun getOrCreate(entity: Entity): DataHolder { + return entities.getOrPut(entity) { DataHolder(entity) } + } + + fun remove(entity: Entity) { + entities.remove(entity) + } + + fun tick() { + entities.entries.removeIf { + it.key.isRemoved + } + } + + fun register(randomInstance: Any) { + val codec = findCodec(randomInstance) + registeredTypes[randomInstance::class.java.name] = codec + } + + fun register(type: Class<*>, codec: cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec<*>) { + registeredTypes[type.name] = codec + } + + fun getCodecFromID(id: String): cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec<*>? { + return registeredTypes[id] ?: CodecHelper.supposedTypes[id] + } + + fun registerScanner() { + val start = System.currentTimeMillis() + CooParticlesConstants.logger.info("正在自动注册 DataHolder") + CooAPIScanner.getWithAnnotation(CooAutoRegister::class.java) + .iterator() + .forEach { target -> + findListenerHandlers(target) + } + val end = System.currentTimeMillis() + CooParticlesConstants.logger.info("DataHolder 注册完成 耗时 ${end - start} ms") + } + + private fun findListenerHandlers(target: SimpleClassInfo) { + val clazz = target.toClass() + if (CodecHelper.supposedTypes[clazz.name] == null) { + return + } + val instance = clazz.declaredConstructors.find { it.parameterCount == 0 }?.newInstance() + ?: return + register(instance) + } + + private fun findCodec(instance: Any): cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec<*> { + val codec = CodecHelper.supposedTypes[instance::class.java.name] + return codec + ?: throw IllegalStateException("DataHolder codec not registered for type: ${instance::class.java.name}") + } + + internal fun applyClient(entity: Entity, packet: PacketDataHolderS2C) { + val store = getOrCreate(entity) + store.cacheAllToggle = packet.cacheAllToggle + store.applyClientSnapshot(packet.decodeData(), packet.fullSync) + } + + fun clearClient() { + entities.values.forEach { it.clearClient() } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt new file mode 100644 index 00000000..b4d831ed --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt @@ -0,0 +1,12 @@ +package cn.coostack.cooparticlesapi.display + +import cn.coostack.cooparticlesapi.annotations.display.handle.DisplayEntityRegistryHelper +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoDisplayEntity(pos: Vec3, world: Level?) : DisplayEntity(pos, world) { + override fun getCodec(): ForgeStreamCodec { + return DisplayEntityRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt new file mode 100644 index 00000000..21c82c79 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt @@ -0,0 +1,292 @@ +package cn.coostack.cooparticlesapi.display + +import cn.coostack.cooparticlesapi.api.NetworkDirtyMarkable +import cn.coostack.cooparticlesapi.api.controler.server.ServerControler +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.Tickable +import cn.coostack.cooparticlesapi.particles.control.RemoveReason +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.MinecraftRendererUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.client.Camera +import net.minecraft.client.renderer.MultiBufferSource +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import org.joml.Matrix4f +import java.util.UUID +import kotlin.math.PI + +abstract class DisplayEntity( + var pos: Vec3, + var world: Level? +) : Controlable, ServerControler, Tickable, NetworkDirtyMarkable { + companion object { + fun encodeBase(data: DisplayEntity, buf: PacketByteBuf) { + buf.writeVec3(data.pos) + buf.writeFloat(data.yaw) + buf.writeFloat(data.pitch) + buf.writeFloat(data.roll) + buf.writeFloat(data.scale) + buf.writeBoolean(data.valid) + buf.writeUUID(data.controlUUID) + } + + fun decodeBase(instance: DisplayEntity, buf: PacketByteBuf) { + instance.apply { + pos = buf.readVec3() + yaw = buf.readFloat() + pitch = buf.readFloat() + roll = buf.readFloat() + scale = buf.readFloat() + valid = buf.readBoolean() + controlUUID = buf.readUUID() + } + } + } + + var controlUUID: UUID = UUID.randomUUID() + + var visibleRange = 256.0 + + var prevPos = pos + + var prevYaw = 0f + + var yaw = 0f + + var prevPitch = 0f + + var pitch = 0f + + var prevRoll = 0f + + var roll = 0f + + var prevScale = 1f + var scale = 1f + + private var valid = true + private var networkStateDirty = true + private var networkFullDirty = true + private var lastNetworkPos = pos + private var lastNetworkYaw = yaw + private var lastNetworkPitch = pitch + private var lastNetworkRoll = roll + private var lastNetworkScale = scale + private var lastNetworkValid = valid + private var pendingRemoteState: RemoteState? = null + private val preTickActions = ArrayList Unit>() + private val postTickActions = ArrayList Unit>() + + var manageRotation = true + + override fun markDirty() { + if (world?.isClientSide != true) { + networkFullDirty = true + networkStateDirty = true + } + } + + internal fun consumeNetworkFullDirty(): Boolean { + val dirty = networkFullDirty + networkFullDirty = false + return dirty + } + + internal fun consumeNetworkStateDirty(): Boolean { + if (lastNetworkPos != pos || lastNetworkYaw != yaw || lastNetworkPitch != pitch || + lastNetworkRoll != roll || lastNetworkScale != scale || lastNetworkValid != valid + ) { + networkStateDirty = true + lastNetworkPos = pos + lastNetworkYaw = yaw + lastNetworkPitch = pitch + lastNetworkRoll = roll + lastNetworkScale = scale + lastNetworkValid = valid + } + val dirty = networkStateDirty + networkStateDirty = false + return dirty + } + + internal fun applyRemoteState(position: Vec3, yaw: Float, pitch: Float, roll: Float, scale: Float) { + pendingRemoteState = RemoteState(position, yaw, pitch, roll, scale) + } + + abstract fun render( + view: Matrix4f, + proj: Matrix4f, + modelMatrixStack: PoseStack, + buffer: MultiBufferSource, + delta: Float, + camera: Camera + ) + + abstract fun getCodec(): ForgeStreamCodec + + open fun canRender( + view: Matrix4f, proj: Matrix4f, modelMatrixStack: PoseStack, lerp: Float, camera: Camera + ): Boolean { + return true + } + + fun rotateFromAngles(stack: PoseStack, delta: Float) { + MinecraftRendererUtil.applyRotation( + stack, yaw(delta), pitch(delta), roll(delta) + ) + } + + fun position(lerp: Float): Vec3 { + return GraphMathHelper.lerp(lerp, prevPos, pos) + } + + fun yaw(lerp: Float): Float { + val delta = Math3DUtil.fixAngle(yaw - prevYaw).toFloat() + return prevYaw + lerp * delta + } + + fun scale(lerp: Float): Float { + return GraphMathHelper.lerp(lerp, prevScale, pos) + } + + fun pitch(lerp: Float): Float { + val delta = Math3DUtil.fixAngle(pitch - prevPitch).toFloat() + return prevPitch + lerp * delta + } + + fun roll(lerp: Float): Float { + val delta = Math3DUtil.fixAngle(roll - prevRoll).toFloat() + return prevRoll + lerp * delta + } + + override fun tick() { + val stableSize = preTickActions.size + var index = 0 + while (index < stableSize) { + preTickActions[index](this) + index++ + } + yaw %= 360 + pitch %= 360 + roll %= 360 + val remoteState = pendingRemoteState + if (remoteState != null) { + this.prevPos = pos + this.prevYaw = yaw + this.prevPitch = pitch + this.prevRoll = roll + this.prevScale = scale + this.pos = remoteState.position + this.yaw = remoteState.yaw + this.pitch = remoteState.pitch + this.roll = remoteState.roll + this.scale = remoteState.scale + pendingRemoteState = null + } else { + this.prevPos = pos + this.prevYaw = yaw + this.prevPitch = pitch + this.prevRoll = roll + this.prevScale = scale + } + postTickActions.forEach { it(this) } + } + + final override fun addPreTickAction(action: DisplayEntity.() -> Unit): Tickable { + preTickActions.add(action) + return this + } + + final override fun addPreTickActionPost(action: DisplayEntity.() -> Unit): Tickable { + postTickActions.add(action) + return this + } + + open fun transformOffset(): Vec3 { + return Vec3.ZERO + } + + open fun renderCenterOffset(): Vec3 { + return Vec3(0.5, 0.5, 0.5) + } + + override fun controlUUID(): UUID { + return controlUUID + } + + override fun rotateToPoint(to: RelativeLocation) { + lookAt(to.toVector()) + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + rotateToPoint(to) + rotateAsAxis(radian) + } + + override fun rotateAsAxis(radian: Double) { + roll += (radian * 180 / PI).toFloat() + } + + fun lookAt(direction: Vec3) { + val yaw = Math3DUtil.getYawFromLocation(direction) * 180 / PI + val pitch = Math3DUtil.getPitchFromLocation(direction) * 180 / PI + this.yaw = yaw.toFloat() + this.pitch = pitch.toFloat() + } + + override fun teleportTo(to: Vec3) { + this.prevPos = to + this.pos = to + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + override fun remove() { + valid = false + } + + override fun remove(reason: RemoveReason) { + remove() + } + + override fun getControlObject(): DisplayEntity { + return this + } + + open fun update(other: DisplayEntity) { + applyRemoteState(other.pos, other.yaw, other.pitch, other.roll, other.scale) + this.valid = other.valid + CodecHelper.updateFields(this, other) + } + + override fun spawn(world: Level, pos: Vec3) { + DisplayEntityManager.spawn( + this.apply { + this.world = world + this.pos = pos + } + ) + } + + override fun isValid(): Boolean { + return valid + } + + override fun getValue(): DisplayEntity { + return this + } + + private data class RemoteState( + val position: Vec3, + val yaw: Float, + val pitch: Float, + val roll: Float, + val scale: Float, + ) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt new file mode 100644 index 00000000..adb6dc27 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt @@ -0,0 +1,239 @@ +package cn.coostack.cooparticlesapi.display + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.extend.plus +import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityStateS2C +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.utils.MinecraftRendererUtil +import com.mojang.blaze3d.vertex.PoseStack +import io.netty.buffer.Unpooled +import net.minecraft.client.Camera +import net.minecraft.client.DeltaTracker +import net.minecraft.client.renderer.MultiBufferSource +import net.minecraft.network.PacketByteBuf +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import org.joml.Matrix4f +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +object DisplayEntityManager { + val clientView = ConcurrentHashMap() + + val serverView = ConcurrentHashMap() + + val playerVisibleSet = ConcurrentHashMap>() + + val registeredTypes = ConcurrentHashMap>() + + fun clientEntityCount(): Int = clientView.size + + fun serverEntityCount(): Int = serverView.size + + fun addClient(entity: DisplayEntity) { + entity.prevPos = entity.pos + entity.prevYaw = entity.yaw + entity.prevPitch = entity.pitch + entity.prevRoll = entity.roll + entity.prevScale = entity.scale + clientView[entity.controlUUID] = entity + } + + fun spawn(entity: DisplayEntity) { + playerVisibleSet.values.forEach { it.remove(entity) } + serverView[entity.controlUUID] = entity + sendCreateOrUpdate(entity) + } + + fun register(randomInstance: DisplayEntity) { + val id = randomInstance::class.java.name + val codec = randomInstance.getCodec() + registeredTypes[id] = codec + } + + fun registerScanner() { + CooParticlesConstants.logger.info("正在自动注册 DisplayEntity") + CooAPIScanner.getWithAnnotation(CooAutoRegister::class.java) + .iterator() + .forEach { + val clazz = it.toClass() + if (!DisplayEntity::class.java.isAssignableFrom(clazz)) { + return@forEach + } + val instance = + clazz.declaredConstructors.find { + it.parameterCount == 0 + }?.newInstance() ?: clazz.getDeclaredConstructor( + Vec3::class.java, + Level::class.java + ) + .newInstance(Vec3.ZERO, null) + register(instance as DisplayEntity) + } + } + + fun render( + view: Matrix4f, + proj: Matrix4f, + modelMatrixStack: PoseStack, + buffer: MultiBufferSource, + delta: DeltaTracker, + camera: Camera + ) { + val lerp = delta.getGameTimeDeltaPartialTick(true) + val iterator = clientView.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val entity = entry.value + if (!entity.isValid()) { + iterator.remove() + continue + } + modelMatrixStack.pushPose() + MinecraftRendererUtil.transformTo( + camera, + entity.position(lerp) + entity.transformOffset(), + modelMatrixStack + ) { + val offset = entity.renderCenterOffset() + if (entity.manageRotation) { + MinecraftRendererUtil.applyAtPoint( + offset, this + ) { + MinecraftRendererUtil.applyRotation( + this, entity.yaw(lerp), entity.pitch(lerp), entity.roll(lerp) + ) + } + } + if (entity.canRender(view, proj, modelMatrixStack, lerp, camera)) { + runCatching { + entity.render(view, proj, modelMatrixStack, buffer, lerp, camera) + }.onFailure { + it.printStackTrace() + } + } + } + modelMatrixStack.popPose() + } + } + + fun tickClient() { + val iterator = clientView.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + entry.value.tick() + if (!entry.value.isValid()) { + iterator.remove() + } + } + } + + fun tickServer() { + val iterator = serverView.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (!entry.value.isValid()) { + sendRemove(entry.value) + iterator.remove() + continue + } + syncVisible(entry.value, false) + entry.value.tick() + } + } + + fun sendCreateOrUpdate(entity: DisplayEntity) { + syncVisible(entity, true) + } + + private fun syncVisible(entity: DisplayEntity, forceUpdate: Boolean) { + val server = CooParticlesAPI.serverOrNull ?: return + val updateTargets = ArrayList() + val removeTargets = ArrayList() + var hasNewTarget = false + server.playerList.players.forEach { player -> + val visible = playerVisibleSet.getOrPut(player.uuid) { HashSet() } + val shouldView = player.level().dimension() == entity.world?.dimension() && + player.position().distanceTo(entity.pos) <= entity.visibleRange + if (entity in visible) { + if (shouldView) { + updateTargets.add(player) + } else { + visible.remove(entity) + removeTargets.add(player) + } + } else if (shouldView) { + visible.add(entity) + updateTargets.add(player) + hasNewTarget = true + } + } + + if (removeTargets.isNotEmpty()) { + val packet = PacketDisplayEntityS2C( + entity.controlUUID, + entity::class.java.name, + ByteArray(0), + true + ) + removeTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(packet, it) } + } + + val fullDirty = entity.consumeNetworkFullDirty() + val stateDirty = entity.consumeNetworkStateDirty() + if (updateTargets.isEmpty()) { + return + } + if (!forceUpdate && !hasNewTarget && !fullDirty) { + if (stateDirty) { + val statePacket = PacketDisplayEntityStateS2C( + entity.controlUUID, + entity.pos, + entity.yaw, + entity.pitch, + entity.roll, + entity.scale, + ) + updateTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(statePacket, it) } + } + return + } + val registryAccess = CooParticlesAPI.registryAccessOrNull ?: return + val uuid = entity.controlUUID + val type = entity::class.java.name + val buf = PacketByteBuf(Unpooled.buffer()) + val data = try { + entity.getCodec().encode(buf, entity) + ByteArray(buf.readableBytes()).also { buf.readBytes(it) } + } finally { + buf.release() + } + val packet = PacketDisplayEntityS2C(uuid, type, data) + updateTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(packet, it) } + } + + fun sendRemove(entity: DisplayEntity) { + val server = CooParticlesAPI.serverOrNull ?: return + val packet = PacketDisplayEntityS2C(entity.controlUUID, entity::class.java.name, ByteArray(0), true) + server.playerList.players.forEach { player -> + val visible = playerVisibleSet[player.uuid] ?: return@forEach + if (visible.remove(entity)) { + CooParticlesServices.SERVER_NETWORK.send(packet, player) + } + } + } + + fun clearClient() { + clientView.clear() + } + + fun clearServer() { + serverView.clear() + playerVisibleSet.clear() + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/items/CooItemForge.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/items/CooItemForge.kt new file mode 100644 index 00000000..2309b563 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/items/CooItemForge.kt @@ -0,0 +1,21 @@ +package cn.coostack.cooparticlesapi.items + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.world.item.Item +import net.minecraftforge.registries.DeferredRegister +import thedarkcolour.kotlinforforge.forge.KotlinForgeForge + +object CooItemForge { + @JvmStatic + val ITEMS: DeferredRegister = DeferredRegister.create(KotlinForgeForge.MOD_EVENT_BUS, CooParticlesConstants.MOD_ID) + + @JvmStatic + fun reg(bus: Any) { + ITEMS.register(bus as net.minecraftforge.eventbus.api.IEventBus) + CooItems.getRegisterItems() + CooItems.itemsWithID.forEach { (key, value) -> + CooParticlesConstants.logger.info("register item :${key.path}") + ITEMS.register(key.path) { value.getItem().get() } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt new file mode 100644 index 00000000..70eeda7a --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt @@ -0,0 +1,206 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistry +import cn.coostack.cooparticlesapi.event.CooEventBus +import cn.coostack.cooparticlesapi.event.events.packet.CooPacketReceiveEvent +import cn.coostack.cooparticlesapi.event.events.packet.CooPacketRequestTimeoutEvent +import cn.coostack.cooparticlesapi.event.events.packet.CooPacketSendEvent +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeC2S +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeS2C +import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkEndpoint +import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkMetrics +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.client.Minecraft +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +object CooClientPacketManager { + const val DEFAULT_TIMEOUT_TICKS = 60 + + private val correlationCounter = AtomicLong(1L) + + private class Pending( + val expectType: Class, + val callback: (CooPacket) -> Unit, + val requestPacket: CooPacket, + var remainingTicks: Int, + val totalTicks: Int, + ) + + private val pending = ConcurrentHashMap() + + @JvmStatic + fun sendTo(packet: CooPacket): Boolean { + return sendInternal(packet, CooPacketKind.NORMAL, 0L, 0) + } + + @JvmStatic + @JvmOverloads + fun request( + packet: CooPacket, + responseType: Class, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + onResponse: (R) -> Unit, + ): Long { + val correlationId = correlationCounter.getAndIncrement() + @Suppress("UNCHECKED_CAST") + pending[correlationId] = Pending( + expectType = responseType, + callback = onResponse as (CooPacket) -> Unit, + requestPacket = packet, + remainingTicks = timeoutTicks, + totalTicks = timeoutTicks, + ) + val ok = sendInternal(packet, CooPacketKind.REQUEST, correlationId, timeoutTicks) + if (!ok) { + pending.remove(correlationId) + return 0L + } + return correlationId + } + + @JvmSynthetic + inline fun request( + packet: CooPacket, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + noinline onResponse: (R) -> Unit, + ): Long = request(packet, R::class.java, timeoutTicks, onResponse) + + @JvmStatic + fun cancelRequest(correlationId: Long): Boolean { + return pending.remove(correlationId) != null + } + + @JvmStatic + fun tick() { + if (pending.isEmpty()) return + val expired = ArrayList>() + val it = pending.entries.iterator() + while (it.hasNext()) { + val entry = it.next() + val p = entry.value + p.remainingTicks-- + if (p.remainingTicks <= 0) { + expired.add(entry.key to p) + it.remove() + } + } + expired.forEach { (id, p) -> + CooEventBus.call( + CooPacketRequestTimeoutEvent( + requestPacket = p.requestPacket, + side = CooPacketRequestTimeoutEvent.Side.CLIENT_TO_SERVER, + targetPlayer = null, + correlationId = id, + timeoutTicks = p.totalTicks, + ) + ) + } + } + + @JvmStatic + fun handleS2C(envelope: CooPacketEnvelopeS2C) { + PerformanceStatusNetworkMetrics.recordReceived( + PerformanceStatusNetworkEndpoint.CLIENT, + envelope.data.size, + ) + val client = Minecraft.getInstance() + client.execute { + handleS2CInternal(envelope) + } + } + + private fun handleS2CInternal(envelope: CooPacketEnvelopeS2C) { + val kind = CooPacketKind.fromId(envelope.kindId) + val packet = CooPacketRegistry.decode(envelope.packetId, envelope.data) + if (packet == null) { + CooParticlesConstants.logger.warn("Received unknown CooPacket: ${envelope.packetId} (kind=$kind)") + return + } + val event = CooEventBus.call( + CooPacketReceiveEvent( + packet = packet, + kind = kind, + side = CooPacketReceiveEvent.Side.CLIENT, + sender = null, + correlationId = envelope.correlationId, + timeoutTicks = envelope.timeoutTicks, + ) + ) + if (event.isCancelled) return + + val ctx = ClientContext(packet, kind, envelope.correlationId, envelope.timeoutTicks) + try { + packet.onClientReceive(ctx) + } catch (e: Throwable) { + CooParticlesConstants.logger.error("CooPacket onClientReceive exception: ${envelope.packetId}", e) + } + + if (kind == CooPacketKind.RESPONSE) { + val pendingEntry = pending.remove(envelope.correlationId) ?: return + if (!pendingEntry.expectType.isInstance(packet)) { + CooParticlesConstants.logger.warn( + "CooPacket response type mismatch: expected ${pendingEntry.expectType.name}, got ${packet::class.java.name}" + ) + return + } + try { + pendingEntry.callback(packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket request callback exception (correlationId=${envelope.correlationId})", + e + ) + } + } + } + + internal fun replyInternal(response: CooPacket, correlationId: Long) { + sendInternal(response, CooPacketKind.RESPONSE, correlationId, 0) + } + + private fun sendInternal( + packet: CooPacket, + kind: CooPacketKind, + correlationId: Long, + timeoutTicks: Int, + ): Boolean { + if (!CooPacketRegistry.isRegistered(packet::class.java)) { + CooParticlesConstants.logger.error( + "CooPacket not registered, cannot send: ${packet::class.java.name} (id=${packet.id()})" + ) + return false + } + val event = CooEventBus.call( + CooPacketSendEvent( + packet = packet, + kind = kind, + side = CooPacketSendEvent.Side.CLIENT_TO_SERVER, + targetPlayer = null, + correlationId = correlationId, + timeoutTicks = timeoutTicks, + ) + ) + if (event.isCancelled) return false + val data = try { + CooPacketRegistry.encode(packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error("CooPacket encode failed: ${packet::class.java.name}", e) + return false + } + val envelope = CooPacketEnvelopeC2S( + kindId = kind.id, + packetId = packet.id(), + correlationId = correlationId, + timeoutTicks = timeoutTicks, + data = data, + ) + CooParticlesServices.CLIENT_NETWORK.send(envelope) + PerformanceStatusNetworkMetrics.recordSent( + PerformanceStatusNetworkEndpoint.CLIENT, + data.size, + ) + return true + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt new file mode 100644 index 00000000..58e7df30 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt @@ -0,0 +1,14 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistryHelper +import net.minecraft.resources.ResourceLocation + +abstract class CooPacket { + abstract fun id(): ResourceLocation + + open fun codec(): CommonStreamCodec = + CooPacketRegistryHelper.generateClassParticleCodec(this::class.java) + + open fun onClientReceive(context: ClientContext) {} + open fun onServerReceive(context: ServerContext) {} +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt new file mode 100644 index 00000000..b41906b1 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt @@ -0,0 +1,297 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistry +import cn.coostack.cooparticlesapi.event.CooEventBus +import cn.coostack.cooparticlesapi.event.events.packet.CooPacketReceiveEvent +import cn.coostack.cooparticlesapi.event.events.packet.CooPacketRequestTimeoutEvent +import cn.coostack.cooparticlesapi.event.events.packet.CooPacketSendEvent +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeC2S +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeS2C +import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkEndpoint +import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkMetrics +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +object CooServerPacketManager { + const val DEFAULT_TIMEOUT_TICKS = 60 + + private val correlationCounter = AtomicLong(1L) + + private class Pending( + val expectType: Class, + val callback: (ServerPlayer, CooPacket) -> Unit, + val targetPlayerUUID: UUID?, + val requestPacket: CooPacket, + var remainingTicks: Int, + val totalTicks: Int, + ) + + private val pending = ConcurrentHashMap() + + @JvmStatic + fun sendTo(player: ServerPlayer, packet: CooPacket): Boolean { + return sendInternal(player, packet, CooPacketKind.NORMAL, 0L, 0) + } + + @JvmStatic + fun sendAll(packet: CooPacket) { + val server = CooParticlesAPI.serverOrNull ?: return + server.playerList.players.forEach { sendTo(it, packet) } + } + + @JvmStatic + fun sendWorlds(worlds: Iterable, packet: CooPacket) { + worlds.forEach { world -> + world.players().forEach { sendTo(it, packet) } + } + } + + @JvmStatic + fun sendWorlds(world: ServerLevel, packet: CooPacket) { + sendWorlds(listOf(world), packet) + } + + @JvmStatic + @JvmOverloads + fun request( + player: ServerPlayer, + packet: CooPacket, + responseType: Class, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + onResponse: (R) -> Unit, + ): Long { + return requestWithSender(player, packet, responseType, timeoutTicks) { _, resp -> onResponse(resp) } + } + + @JvmSynthetic + inline fun request( + player: ServerPlayer, + packet: CooPacket, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + noinline onResponse: (R) -> Unit, + ): Long = request(player, packet, R::class.java, timeoutTicks, onResponse) + + @JvmStatic + fun requestWithSender( + player: ServerPlayer, + packet: CooPacket, + responseType: Class, + timeoutTicks: Int, + onResponse: (ServerPlayer, R) -> Unit, + ): Long { + val correlationId = correlationCounter.getAndIncrement() + @Suppress("UNCHECKED_CAST") + pending[correlationId] = Pending( + expectType = responseType, + callback = { sender, p -> onResponse(sender, p as R) }, + targetPlayerUUID = player.uuid, + requestPacket = packet, + remainingTicks = timeoutTicks, + totalTicks = timeoutTicks, + ) + val ok = sendInternal(player, packet, CooPacketKind.REQUEST, correlationId, timeoutTicks) + if (!ok) { + pending.remove(correlationId) + return 0L + } + return correlationId + } + + @JvmStatic + @JvmOverloads + fun requestAll( + packet: CooPacket, + responseType: Class, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + onResponse: (ServerPlayer, R) -> Unit, + ): List { + val server = CooParticlesAPI.serverOrNull ?: return emptyList() + return server.playerList.players.map { player -> + requestWithSender(player, packet, responseType, timeoutTicks, onResponse) + } + } + + @JvmSynthetic + inline fun requestAll( + packet: CooPacket, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + noinline onResponse: (ServerPlayer, R) -> Unit, + ): List = requestAll(packet, R::class.java, timeoutTicks, onResponse) + + @JvmStatic + @JvmOverloads + fun requestWorlds( + worlds: Iterable, + packet: CooPacket, + responseType: Class, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + onResponse: (ServerPlayer, R) -> Unit, + ): List { + val ids = ArrayList() + worlds.forEach { world -> + world.players().forEach { player -> + ids.add(requestWithSender(player, packet, responseType, timeoutTicks, onResponse)) + } + } + return ids + } + + @JvmSynthetic + inline fun requestWorlds( + worlds: Iterable, + packet: CooPacket, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + noinline onResponse: (ServerPlayer, R) -> Unit, + ): List = requestWorlds(worlds, R::class.java, timeoutTicks, onResponse) + + @JvmStatic + fun cancelRequest(correlationId: Long): Boolean { + return pending.remove(correlationId) != null + } + + @JvmStatic + fun tick() { + if (pending.isEmpty()) return + val expired = ArrayList>() + val it = pending.entries.iterator() + while (it.hasNext()) { + val entry = it.next() + val p = entry.value + p.remainingTicks-- + if (p.remainingTicks <= 0) { + expired.add(entry.key to p) + it.remove() + } + } + if (expired.isEmpty()) return + val server = CooParticlesAPI.serverOrNull + expired.forEach { (id, p) -> + val target = if (server != null && p.targetPlayerUUID != null) { + server.playerList.getPlayer(p.targetPlayerUUID) + } else null + CooEventBus.call( + CooPacketRequestTimeoutEvent( + requestPacket = p.requestPacket, + side = CooPacketRequestTimeoutEvent.Side.SERVER_TO_CLIENT, + targetPlayer = target, + correlationId = id, + timeoutTicks = p.totalTicks, + ) + ) + } + } + + @JvmStatic + fun handleC2S(envelope: CooPacketEnvelopeC2S, sender: ServerPlayer) { + PerformanceStatusNetworkMetrics.recordReceived( + PerformanceStatusNetworkEndpoint.SERVER, + envelope.data.size, + ) + val server = sender.server + server.execute { + handleC2SInternal(envelope, sender) + } + } + + private fun handleC2SInternal(envelope: CooPacketEnvelopeC2S, sender: ServerPlayer) { + val kind = CooPacketKind.fromId(envelope.kindId) + val packet = CooPacketRegistry.decode(envelope.packetId, envelope.data) + if (packet == null) { + CooParticlesConstants.logger.warn( + "Received unknown CooPacket: ${envelope.packetId} (kind=$kind, sender=${sender.gameProfile.name})" + ) + return + } + val event = CooEventBus.call( + CooPacketReceiveEvent( + packet = packet, + kind = kind, + side = CooPacketReceiveEvent.Side.SERVER, + sender = sender, + correlationId = envelope.correlationId, + timeoutTicks = envelope.timeoutTicks, + ) + ) + if (event.isCancelled) return + + val ctx = ServerContext(sender, packet, kind, envelope.correlationId, envelope.timeoutTicks) + try { + packet.onServerReceive(ctx) + } catch (e: Throwable) { + CooParticlesConstants.logger.error("CooPacket onServerReceive exception: ${envelope.packetId}", e) + } + + if (kind == CooPacketKind.RESPONSE) { + val pendingEntry = pending.remove(envelope.correlationId) ?: return + if (!pendingEntry.expectType.isInstance(packet)) { + CooParticlesConstants.logger.warn( + "CooPacket response type mismatch: expected ${pendingEntry.expectType.name}, got ${packet::class.java.name}" + ) + return + } + try { + pendingEntry.callback(sender, packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket request callback exception (correlationId=${envelope.correlationId})", + e + ) + } + } + } + + internal fun replyInternal(target: ServerPlayer, response: CooPacket, correlationId: Long) { + sendInternal(target, response, CooPacketKind.RESPONSE, correlationId, 0) + } + + private fun sendInternal( + player: ServerPlayer, + packet: CooPacket, + kind: CooPacketKind, + correlationId: Long, + timeoutTicks: Int, + ): Boolean { + if (!CooPacketRegistry.isRegistered(packet::class.java)) { + CooParticlesConstants.logger.error( + "CooPacket not registered, cannot send: ${packet::class.java.name} (id=${packet.id()})" + ) + return false + } + val event = CooEventBus.call( + CooPacketSendEvent( + packet = packet, + kind = kind, + side = CooPacketSendEvent.Side.SERVER_TO_CLIENT, + targetPlayer = player, + correlationId = correlationId, + timeoutTicks = timeoutTicks, + ) + ) + if (event.isCancelled) return false + val data = try { + CooPacketRegistry.encode(packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error("CooPacket encode failed: ${packet::class.java.name}", e) + return false + } + val envelope = CooPacketEnvelopeS2C( + kindId = kind.id, + packetId = packet.id(), + correlationId = correlationId, + timeoutTicks = timeoutTicks, + data = data, + ) + CooParticlesServices.SERVER_NETWORK.send(envelope, player) + PerformanceStatusNetworkMetrics.recordSent( + PerformanceStatusNetworkEndpoint.SERVER, + data.size, + ) + return true + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt new file mode 100644 index 00000000..8a9a7e74 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt @@ -0,0 +1,35 @@ +package cn.coostack.cooparticlesapi.network.packet.api.envelope + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class CooPacketEnvelopeC2S( + val kindId: Int, + val packetId: ResourceLocation, + val correlationId: Long, + val timeoutTicks: Int, + val data: ByteArray, +) { + companion object { + @JvmStatic + fun write(buf: PacketByteBuf, packet: CooPacketEnvelopeC2S) { + buf.writeVarInt(packet.kindId) + buf.writeResourceLocation(packet.packetId) + buf.writeLong(packet.correlationId) + buf.writeVarInt(packet.timeoutTicks) + buf.writeByteArray(packet.data) + } + + @JvmStatic + fun read(buf: PacketByteBuf): CooPacketEnvelopeC2S { + return CooPacketEnvelopeC2S( + kindId = buf.readVarInt(), + packetId = buf.readResourceLocation(), + correlationId = buf.readLong(), + timeoutTicks = buf.readVarInt(), + data = buf.readByteArray() + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt new file mode 100644 index 00000000..100bb0ac --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt @@ -0,0 +1,35 @@ +package cn.coostack.cooparticlesapi.network.packet.api.envelope + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class CooPacketEnvelopeS2C( + val kindId: Int, + val packetId: ResourceLocation, + val correlationId: Long, + val timeoutTicks: Int, + val data: ByteArray, +) { + companion object { + @JvmStatic + fun write(buf: PacketByteBuf, packet: CooPacketEnvelopeS2C) { + buf.writeVarInt(packet.kindId) + buf.writeResourceLocation(packet.packetId) + buf.writeLong(packet.correlationId) + buf.writeVarInt(packet.timeoutTicks) + buf.writeByteArray(packet.data) + } + + @JvmStatic + fun read(buf: PacketByteBuf): CooPacketEnvelopeS2C { + return CooPacketEnvelopeS2C( + kindId = buf.readVarInt(), + packetId = buf.readResourceLocation(), + correlationId = buf.readLong(), + timeoutTicks = buf.readVarInt(), + data = buf.readByteArray() + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt new file mode 100644 index 00000000..7463af62 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.packet.client + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.event.events.key.KeyActionBatch +import cn.coostack.cooparticlesapi.event.events.key.KeyActionData +import cn.coostack.cooparticlesapi.event.events.key.KeyActionType +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class PacketKeyActionC2S( + val keyActions: KeyActionBatch +) { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_action") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_action") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + val entries = packet.keyActions.entries + buf.writeVarInt(entries.size) + entries.forEach { entry -> + buf.writeResourceLocation(entry.keyId) + buf.writeVarInt(entry.actions.size) + entry.actions.forEach { action -> + buf.writeInt(action.id) + } + buf.writeInt(entry.pressTick) + buf.writeBoolean(entry.released) + } + }, { buf -> + val entryCount = buf.readVarInt() + val entries = ArrayList>(entryCount) + repeat(entryCount) { + val keyId = buf.readResourceLocation() + val actionCount = buf.readVarInt() + val actions = ArrayList(actionCount) + repeat(actionCount) { + actions.add(KeyActionType.fromId(buf.readInt())) + } + val pressTick = buf.readInt() + val released = buf.readBoolean() + entries.add(KeyActionData(keyId, actions, pressTick, released)) + } + PacketKeyActionC2S(KeyActionBatch(entries)) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt new file mode 100644 index 00000000..5e68d28f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt @@ -0,0 +1,203 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +class PacketCameraShakeS2C( + val operation: CameraOperation, + val range: Double, + val origin: Vec3, + val amplitude: Double, + val tick: Int, + val frequency: Double, + val attenuateByDistance: Boolean, + val position: Vec3, + val yawOffset: Float, + val pitchOffset: Float, + val instant: Boolean +) { + constructor(range: Double, origin: Vec3, amplitude: Double, tick: Int) : this( + CameraOperation.SHAKE, + range, + origin, + amplitude, + tick, + 1.0, + false, + Vec3.ZERO, + 0f, + 0f, + false + ) + + enum class CameraOperation(val id: Int) { + SHAKE(0), + SET_OFFSET(1), + RESET_OFFSET(2), + FORCE_POSITION(3), + RESET_FORCE_POSITION(4), + RESET_ALL(5) + } + + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "camara_shake") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "camara_shake") + + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeByte(packet.operation.id) + buf.writeDouble(packet.range) + buf.writeVec3(packet.origin) + buf.writeDouble(packet.amplitude) + buf.writeInt(packet.tick) + buf.writeDouble(packet.frequency) + buf.writeBoolean(packet.attenuateByDistance) + buf.writeVec3(packet.position) + buf.writeFloat(packet.yawOffset) + buf.writeFloat(packet.pitchOffset) + buf.writeBoolean(packet.instant) + }, { buf -> + val operation = operationFromId(buf.readUnsignedByte().toInt()) + val range = buf.readDouble() + val origin = buf.readVec3() + val amplitude = buf.readDouble() + val tick = buf.readInt() + val frequency = buf.readDouble() + val attenuateByDistance = buf.readBoolean() + val position = buf.readVec3() + val yawOffset = buf.readFloat() + val pitchOffset = buf.readFloat() + val instant = buf.readBoolean() + PacketCameraShakeS2C( + operation, + range, + origin, + amplitude, + tick, + frequency, + attenuateByDistance, + position, + yawOffset, + pitchOffset, + instant + ) + }) + + fun shake(range: Double, origin: Vec3, amplitude: Double, tick: Int): PacketCameraShakeS2C { + return shake(range, origin, amplitude, tick, 1.0, false) + } + + fun shake( + range: Double, + origin: Vec3, + amplitude: Double, + tick: Int, + frequency: Double, + attenuateByDistance: Boolean + ): PacketCameraShakeS2C { + return PacketCameraShakeS2C( + CameraOperation.SHAKE, + range, + origin, + amplitude, + tick, + frequency, + attenuateByDistance, + Vec3.ZERO, + 0f, + 0f, + false + ) + } + + fun setOffset( + positionOffset: Vec3, + yawOffset: Float = 0f, + pitchOffset: Float = 0f, + instant: Boolean = false + ): PacketCameraShakeS2C { + return PacketCameraShakeS2C( + CameraOperation.SET_OFFSET, + -1.0, + Vec3.ZERO, + 0.0, + 0, + 1.0, + false, + positionOffset, + yawOffset, + pitchOffset, + instant + ) + } + + fun resetOffset(instant: Boolean = false): PacketCameraShakeS2C { + return PacketCameraShakeS2C( + CameraOperation.RESET_OFFSET, + -1.0, + Vec3.ZERO, + 0.0, + 0, + 1.0, + false, + Vec3.ZERO, + 0f, + 0f, + instant + ) + } + + fun forcePosition(position: Vec3, instant: Boolean = false): PacketCameraShakeS2C { + return PacketCameraShakeS2C( + CameraOperation.FORCE_POSITION, + -1.0, + Vec3.ZERO, + 0.0, + 0, + 1.0, + false, + position, + 0f, + 0f, + instant + ) + } + + fun resetForcePosition(instant: Boolean = false): PacketCameraShakeS2C { + return PacketCameraShakeS2C( + CameraOperation.RESET_FORCE_POSITION, + -1.0, + Vec3.ZERO, + 0.0, + 0, + 1.0, + false, + Vec3.ZERO, + 0f, + 0f, + instant + ) + } + + fun resetAll(instant: Boolean = false): PacketCameraShakeS2C { + return PacketCameraShakeS2C( + CameraOperation.RESET_ALL, + -1.0, + Vec3.ZERO, + 0.0, + 0, + 1.0, + false, + Vec3.ZERO, + 0f, + 0f, + instant + ) + } + + private fun operationFromId(id: Int): CameraOperation { + return CameraOperation.entries.firstOrNull { it.id == id } ?: CameraOperation.SHAKE + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt new file mode 100644 index 00000000..c04c04fe --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +object PacketClearClientStateS2C { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "clear_client_state") + + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "clear_client_state") + val CODEC = ForgeStreamCodec.of({ _, _ -> }, { _ -> PacketClearClientStateS2C }) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt new file mode 100644 index 00000000..d919c2c0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt @@ -0,0 +1,89 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class PacketDataHolderS2C( + val entityId: Int, + val cacheAllToggle: Boolean, + val fullSync: Boolean, + val entries: List +) { + data class Entry( + val key: String, + val type: String, + val data: ByteArray + ) + + companion object { + private val id = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "data_holder") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "data_holder") + val CODEC = ForgeStreamCodec.of({ buf, packet -> + buf.writeInt(packet.entityId) + buf.writeBoolean(packet.cacheAllToggle) + buf.writeBoolean(packet.fullSync) + buf.writeInt(packet.entries.size) + packet.entries.forEach { entry -> + buf.writeUtf(entry.key) + buf.writeUtf(entry.type) + buf.writeInt(entry.data.size) + buf.writeBytes(entry.data) + } + }, { buf -> + val entityId = buf.readInt() + val cacheAllToggle = buf.readBoolean() + val fullSync = buf.readBoolean() + val size = buf.readInt() + val entries = ArrayList(size) + repeat(size) { + val key = buf.readUtf() + val type = buf.readUtf() + val dataSize = buf.readInt() + val bytes = ByteArray(dataSize) + buf.readBytes(bytes) + entries.add(Entry(key, type, bytes)) + } + PacketDataHolderS2C(entityId, cacheAllToggle, fullSync, entries) + }) + + fun fromEntity(entity: net.minecraft.world.entity.Entity, holder: cn.coostack.cooparticlesapi.data.holder.DataHolder, fullSync: Boolean): PacketDataHolderS2C { + val entries = holder.snapshotServer().map { (key, value) -> + Entry(key.id.toString(), key.targetType.name, encodeValue(key.targetType.name, value, entity)) + } + return PacketDataHolderS2C(entity.id, holder.cacheAllToggle, fullSync, entries) + } + + private fun encodeValue(type: String, value: Any, entity: net.minecraft.world.entity.Entity): ByteArray { + val codec = cn.coostack.cooparticlesapi.data.holder.DataHolderManager.getCodecFromID(type) + ?: throw IllegalStateException("DataHolder codec not registered for type: $type") + val buf = PacketByteBuf(io.netty.buffer.Unpooled.buffer()) + @Suppress("UNCHECKED_CAST") + (codec as CommonStreamCodec).encode(buf, value) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + buf.release() + return data + } + } + + fun decodeData(): Map, Any> { + val entity = net.minecraft.client.Minecraft.getInstance().level?.getEntity(entityId) + ?: return emptyMap() + return entries.associate { entry -> + val keyType = runCatching { + Class.forName(entry.type) + }.getOrDefault(Any::class.java) + val key = cn.coostack.cooparticlesapi.data.holder.DataHolderKey.ofRaw(keyType, net.minecraft.resources.ResourceLocation.parse(entry.key)) + key to decodeValue(entry.type, entry.data, entity) + } + } + + private fun decodeValue(type: String, data: ByteArray, entity: net.minecraft.world.entity.Entity): Any { + val codec = cn.coostack.cooparticlesapi.data.holder.DataHolderManager.getCodecFromID(type) + ?: throw IllegalStateException("DataHolder codec not registered for type: $type") + val buf = PacketByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data)) + @Suppress("UNCHECKED_CAST") + return (codec as CommonStreamCodec).decode(buf) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt new file mode 100644 index 00000000..a7528349 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt @@ -0,0 +1,32 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketDisplayEntityS2C( + val uuid: UUID, + val type: String, + val data: ByteArray, + val removed: Boolean = false +) { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUtf(packet.type) + buf.writeUUID(packet.uuid) + buf.writeBoolean(packet.removed) + buf.writeInt(packet.data.size) + buf.writeBytes(packet.data) + }, { buf -> + val type = buf.readUtf() + val uuid = buf.readUUID() + val removed = buf.readBoolean() + val size = buf.readInt() + val data = ByteArray(size).also { buf.readBytes(it) } + PacketDisplayEntityS2C(uuid, type, data, removed) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt new file mode 100644 index 00000000..0c261e1b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt @@ -0,0 +1,39 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +class PacketDisplayEntityStateS2C( + val uuid: UUID, + val position: Vec3, + val yaw: Float, + val pitch: Float, + val roll: Float, + val scale: Float, +) { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity_state") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity_state") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeVec3(packet.position) + buf.writeFloat(packet.yaw) + buf.writeFloat(packet.pitch) + buf.writeFloat(packet.roll) + buf.writeFloat(packet.scale) + }, { buf -> + PacketDisplayEntityStateS2C( + buf.readUUID(), + buf.readVec3(), + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + buf.readFloat(), + ) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt new file mode 100644 index 00000000..bfaac96f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt @@ -0,0 +1,19 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class PacketKeyBindingCountdownS2C(val key: ResourceLocation, val cd: Int) { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_binding_countdown") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_binding_countdown") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeResourceLocation(packet.key) + buf.writeInt(packet.cd) + }, { buf -> + PacketKeyBindingCountdownS2C(buf.readResourceLocation(), buf.readInt()) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt new file mode 100644 index 00000000..8826c749 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt @@ -0,0 +1,38 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.core.particles.ParticleTypes +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +class PacketParticleBatchS2C( + val type: ParticleOptions, + val positions: List, + val velocity: Vec3, +) { + init { + require(positions.size <= MAX_PARTICLES) { "particle batch exceeds $MAX_PARTICLES entries" } + } + + companion object { + const val MAX_PARTICLES = 4096 + + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_batch") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_batch") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + ParticleTypes.STREAM_CODEC.encode(buf, packet.type) + buf.writeVec3(packet.velocity) + buf.writeVarInt(packet.positions.size) + packet.positions.forEach { buf.writeVec3(it) } + }, { buf -> + val type = ParticleTypes.STREAM_CODEC.decode(buf) + val velocity = buf.readVec3() + val size = buf.readVarInt() + require(size in 0..MAX_PARTICLES) { "invalid particle batch size: $size" } + val positions = List(size) { buf.readVec3() } + PacketParticleBatchS2C(type, positions, velocity) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt new file mode 100644 index 00000000..94c1f892 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt @@ -0,0 +1,30 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +class PacketParticleCompositionRotateS2C( + val uuid: UUID, + val direction: Vec3?, + val rollDelta: Double +) { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_rotate") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_rotate") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeBoolean(packet.direction != null) + packet.direction?.let { buf.writeVec3(it) } + buf.writeDouble(packet.rollDelta) + }, { buf -> + val uuid = buf.readUUID() + val direction = if (buf.readBoolean()) buf.readVec3() else null + val rollDelta = buf.readDouble() + PacketParticleCompositionRotateS2C(uuid, direction, rollDelta) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt new file mode 100644 index 00000000..9bfa6c3e --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt @@ -0,0 +1,36 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleCompositionS2C(val uuid: UUID, val type: String, val data: ByteArray) { + var distanceRemove = false + var recreate = false + + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUtf(packet.type) + buf.writeUUID(packet.uuid) + buf.writeBoolean(packet.distanceRemove) + buf.writeBoolean(packet.recreate) + buf.writeInt(packet.data.size) + buf.writeBytes(packet.data) + }, { buf -> + val type = buf.readUtf() + val uuid = buf.readUUID() + val distanceRemove = buf.readBoolean() + val recreate = buf.readBoolean() + val size = buf.readInt() + val data = ByteArray(size).also { buf.readBytes(it) } + PacketParticleCompositionS2C(uuid, type, data).apply { + this.distanceRemove = distanceRemove + this.recreate = recreate + } + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt new file mode 100644 index 00000000..2680be04 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt @@ -0,0 +1,42 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +class PacketParticleCompositionStateS2C( + val uuid: UUID, + val position: Vec3, + val visibleRange: Double, + val scale: Double, + val displayStatus: Int, + val closedInterval: Int, + val current: Int, +) { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_state") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_state") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeVec3(packet.position) + buf.writeDouble(packet.visibleRange) + buf.writeDouble(packet.scale) + buf.writeVarInt(packet.displayStatus) + buf.writeVarInt(packet.closedInterval) + buf.writeVarInt(packet.current) + }, { buf -> + PacketParticleCompositionStateS2C( + buf.readUUID(), + buf.readVec3(), + buf.readDouble(), + buf.readDouble(), + buf.readVarInt(), + buf.readVarInt(), + buf.readVarInt(), + ) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt new file mode 100644 index 00000000..b71f7144 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt @@ -0,0 +1,57 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleEmittersS2C( + val emitterID: String, + val emitterUUID: UUID, + val emitterData: ByteArray, + val type: PacketType +) { + enum class PacketType(val id: Int) { + CREATE(0), + REMOVE(1), + CHANGE(2); + + companion object { + @JvmStatic + fun fromID(id: Int): PacketType { + return when (id) { + 0 -> CREATE + 1 -> REMOVE + 2 -> CHANGE + else -> CREATE + } + } + } + } + + companion object { + private val id = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_emitters") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_emitters") + + val CODEC = ForgeStreamCodec.of({ buf, packet -> + val emitterID = packet.emitterID + buf.writeInt(packet.type.id) + buf.writeUtf(emitterID) + buf.writeUUID(packet.emitterUUID) + buf.writeInt(packet.emitterData.size) + buf.writeBytes(packet.emitterData) + }, { buf -> + val packetTypeID = buf.readInt() + val emitterID = buf.readUtf() + val emitterUUID = buf.readUUID() + val size = buf.readInt() + PacketParticleEmittersS2C( + emitterID, + emitterUUID, + ByteArray(size).also { buf.readBytes(it) }, + PacketType.fromID(packetTypeID) + ) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt new file mode 100644 index 00000000..2068e292 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt @@ -0,0 +1,72 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleGroupS2C( + val uuid: UUID, + val type: cn.coostack.cooparticlesapi.particles.control.ControlType, + val args: Map> +) { + enum class PacketArgsType(val ofArgs: String) { + POS("pos"), + CURRENT_TICK("current_tick"), + MAX_TICK("max_tick"), + ROTATE_TO("rotate_to"), + ROTATE_AXIS("rotate_axis"), + INVOKE("invoke"), + AXIS("axis"), + SCALE("scale"), + GROUP_TYPE("groupType"); + + companion object { + fun fromArgsName(value: String): PacketArgsType { + return when (value) { + "pos" -> POS + "current_tick" -> CURRENT_TICK + "max_tick" -> MAX_TICK + "rotate_to" -> ROTATE_TO + "rotate_axis" -> ROTATE_AXIS + "invoke" -> INVOKE + "scale" -> SCALE + "groupType" -> GROUP_TYPE + else -> INVOKE + } + } + } + } + + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_group") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_group") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeInt(packet.type.id) + packet.args.forEach { (t, u) -> + val encode = cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers.encode(u) + val len = encode.size + buf.writeInt(len) + buf.writeUtf(t) + buf.writeBytes(encode) + } + }, { buf -> + val args = HashMap>() + val uuid = buf.readUUID() + val id = buf.readInt() + val type = cn.coostack.cooparticlesapi.particles.control.ControlType.Companion.getTypeById(id) + while (buf.readableBytes() != 0) { + val len = buf.readInt() + val key = buf.readUtf() + val value = ByteArray(len) + buf.readBytes(value) + val decode = cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers.decodeToBuffer(value) + args[key] = decode + } + PacketParticleGroupS2C( + uuid, type, args + ) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt new file mode 100644 index 00000000..adc24de5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt @@ -0,0 +1,32 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.codec.ForgeCodecHelper +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +class PacketParticleS2C( + val type: ParticleOptions, + val pos: Vec3, + val velocity: Vec3, +) { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle") + + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeVec3(packet.pos) + buf.writeVec3(packet.velocity) + ForgeCodecHelper.particleCodecOf(packet.type).encode(buf, packet.type) + }, { buf -> + val pos = buf.readVec3() + val velocity = buf.readVec3() + val type = ForgeCodecHelper.particleCodecOf(ParticleOptions {}.writeToPacket(PacketByteBuf(net.minecraftforge.network.NetworkEvent.INSTANCE.get()?.get() ?: net.io.netty.buffer.Unpooled.buffer())).let { + net.minecraft.core.particles.ParticleTypes.END_ROD + }).decode(buf) + PacketParticleS2C(type, pos, velocity) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt new file mode 100644 index 00000000..61ab8cf5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt @@ -0,0 +1,45 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleStyleS2C( + val uuid: UUID, + val type: cn.coostack.cooparticlesapi.particles.control.ControlType, + val args: Map> +) { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_style") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_style") + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeInt(packet.type.id) + buf.writeInt(packet.args.size) + packet.args.forEach { (t, u) -> + val encode = cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers.encode(u) + val len = encode.size + buf.writeInt(len) + buf.writeUtf(t) + buf.writeBytes(encode) + } + }, { buf -> + val args = HashMap>() + val uuid = buf.readUUID() + val id = buf.readInt() + val type = cn.coostack.cooparticlesapi.particles.control.ControlType.getTypeById(id) + val argsCount = buf.readInt() + repeat(argsCount) { + val len = buf.readInt() + val key = buf.readUtf() + val byteBuf = buf.readBytes(len) + val decode = cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers.decodeToBuffer(byteBuf) + args[key] = decode + } + PacketParticleStyleS2C( + uuid, type, args + ) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt new file mode 100644 index 00000000..10e45036 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt @@ -0,0 +1,49 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketRenderEntityS2C(var uuid: UUID, var entityData: ByteArray, var id: ResourceLocation, var method: Method) { + enum class Method(val id: Int) { + CREATE(0), + TOGGLE(1), + REMOVE(2); + + companion object { + fun idOf(id: Int): Method { + return when (id) { + CREATE.id -> CREATE + TOGGLE.id -> TOGGLE + REMOVE.id -> REMOVE + else -> CREATE + } + } + } + } + + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_entity_packet") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_entity_packet") + + val CODEC = ForgeStreamCodec.of({ packet, buf -> + val entity = packet.entityData + buf.writeInt(packet.method.id) + buf.writeUUID(packet.uuid) + buf.writeResourceLocation(packet.id) + buf.writeInt(entity.size) + buf.writeBytes(entity) + }, { buf -> + val method = buf.readInt() + val uuid = buf.readUUID() + val id = buf.readResourceLocation() + val size = buf.readInt() + val entity = buf.readBytes(size) + val bytes = ByteArray(size) + entity.readBytes(bytes) + PacketRenderEntityS2C(uuid, bytes, id, Method.idOf(method)) + }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt new file mode 100644 index 00000000..6494b121 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt @@ -0,0 +1,85 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.renderer.post.PostEffectBinding +import cn.coostack.cooparticlesapi.renderer.post.PostEffectLifecycle +import cn.coostack.cooparticlesapi.renderer.post.PostEffectParams +import cn.coostack.cooparticlesapi.renderer.post.SyncedPostEffectState +import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class PacketRendererPostEffectS2C private constructor( + internal val operation: Operation, + internal val state: SyncedPostEffectState?, + internal val instanceId: String +) { + internal enum class Operation(val id: Int) { + CREATE(0), + UPDATE(1), + REMOVE(2); + + companion object { + fun idOf(id: Int): Operation { + return entries.firstOrNull { it.id == id } ?: CREATE + } + } + } + + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_post_effect_packet") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_post_effect_packet") + + val CODEC = ForgeStreamCodec.of({ packet, buf -> + buf.writeInt(packet.operation.id) + buf.writeUtf(packet.instanceId) + buf.writeBoolean(packet.state != null) + packet.state?.write(buf) + }, { buf -> + val operation = Operation.idOf(buf.readInt()) + val instanceId = buf.readUtf() + val state = if (buf.readBoolean()) readState(buf) else null + PacketRendererPostEffectS2C(operation, state, instanceId) + }) + + internal fun create(state: SyncedPostEffectState): PacketRendererPostEffectS2C { + return PacketRendererPostEffectS2C(Operation.CREATE, state, state.instanceId) + } + + internal fun update(state: SyncedPostEffectState): PacketRendererPostEffectS2C { + return PacketRendererPostEffectS2C(Operation.UPDATE, state, state.instanceId) + } + + internal fun remove(instanceId: String): PacketRendererPostEffectS2C { + return PacketRendererPostEffectS2C(Operation.REMOVE, null, instanceId) + } + + private fun SyncedPostEffectState.write(buf: PacketByteBuf) { + buf.writeResourceLocation(effectType) + buf.writeUtf(instanceId) + PostEffectBinding.writeTyped(buf, binding) + lifecycle.write(buf) + params.write(buf) + buf.writeInt(uniformNames.size) + uniformNames.sorted().forEach(buf::writeUtf) + buf.writeUtf(sourceId) + buf.writeInt(priority) + } + + private fun readState(buf: PacketByteBuf): SyncedPostEffectState { + return SyncedPostEffectState( + effectType = buf.readResourceLocation(), + instanceId = buf.readUtf(), + binding = PostEffectBinding.readTyped(buf), + lifecycle = PostEffectLifecycle.read(buf), + params = PostEffectParams.read(buf), + uniformNames = buildSet { + repeat(buf.readInt()) { add(buf.readUtf()) } + }, + sourceId = buf.readUtf(), + priority = buf.readInt() + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt new file mode 100644 index 00000000..e9ed2546 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt @@ -0,0 +1,237 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.sounds.SoundSource +import net.minecraft.world.phys.Vec3 + +class PacketSoundInstanceS2C( + val action: Action, + val key: String, + val sound: ResourceLocation, + val source: SoundSource, + val entityId: Int, + val pos: Vec3, + val volume: Float, + val pitch: Float, + val looping: Boolean, + val relative: Boolean, + val stopImmediately: Boolean, + val duckVolume: Float, + val duckRange: Double, + val whitelistSounds: Set, + val whitelistSources: Set, + val whitelistKeys: Set +) { + enum class Action { + PLAY, + UPDATE, + STOP, + DUCK_START, + DUCK_UPDATE, + DUCK_STOP + } + + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_instance") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_instance") + + private val emptySound = ResourceLocation.withDefaultNamespace("empty") + + val CODEC = ForgeStreamCodec.of( + { packet, buf -> + buf.writeEnum(packet.action) + buf.writeUtf(packet.key) + buf.writeResourceLocation(packet.sound) + buf.writeEnum(packet.source) + buf.writeInt(packet.entityId) + buf.writeVec3(packet.pos) + buf.writeFloat(packet.volume) + buf.writeFloat(packet.pitch) + buf.writeBoolean(packet.looping) + buf.writeBoolean(packet.relative) + buf.writeBoolean(packet.stopImmediately) + buf.writeFloat(packet.duckVolume) + buf.writeDouble(packet.duckRange) + writeResourceLocationSet(buf, packet.whitelistSounds) + writeSoundSourceSet(buf, packet.whitelistSources) + writeStringSet(buf, packet.whitelistKeys) + }, + { buf -> + PacketSoundInstanceS2C( + action = buf.readEnum(Action::class.java), + key = buf.readUtf(), + sound = buf.readResourceLocation(), + source = buf.readEnum(SoundSource::class.java), + entityId = buf.readInt(), + pos = buf.readVec3(), + volume = buf.readFloat(), + pitch = buf.readFloat(), + looping = buf.readBoolean(), + relative = buf.readBoolean(), + stopImmediately = buf.readBoolean(), + duckVolume = buf.readFloat(), + duckRange = buf.readDouble(), + whitelistSounds = readResourceLocationSet(buf), + whitelistSources = readSoundSourceSet(buf), + whitelistKeys = readStringSet(buf) + ) + } + ) + + fun play( + key: String, + sound: ResourceLocation, + source: SoundSource, + entityId: Int, + pos: Vec3, + volume: Float, + pitch: Float, + looping: Boolean, + relative: Boolean = false + ): PacketSoundInstanceS2C { + return PacketSoundInstanceS2C( + Action.PLAY, + key, + sound, + source, + entityId, + pos, + volume, + pitch, + looping, + relative, + true, + 1f, + -1.0, + emptySet(), + emptySet(), + emptySet() + ) + } + + fun update( + key: String, + entityId: Int, + pos: Vec3, + volume: Float, + pitch: Float, + looping: Boolean = false, + relative: Boolean = false + ): PacketSoundInstanceS2C { + return PacketSoundInstanceS2C( + Action.UPDATE, + key, + emptySound, + SoundSource.MASTER, + entityId, + pos, + volume, + pitch, + looping, + relative, + true, + 1f, + -1.0, + emptySet(), + emptySet(), + emptySet() + ) + } + + fun stop(key: String, interrupt: Boolean = true): PacketSoundInstanceS2C { + return PacketSoundInstanceS2C( + Action.STOP, + key, + emptySound, + SoundSource.MASTER, + -1, + Vec3.ZERO, + 0f, + 1f, + false, + false, + interrupt, + 1f, + -1.0, + emptySet(), + emptySet(), + emptySet() + ) + } + + fun duck( + action: Action, + key: String, + entityId: Int, + pos: Vec3, + volumeMultiplier: Float, + range: Double = -1.0, + whitelistSounds: Set = HashSet(), + whitelistSources: Set = HashSet(), + whitelistKeys: Set = HashSet() + ): PacketSoundInstanceS2C { + require(action == Action.DUCK_START || action == Action.DUCK_UPDATE || action == Action.DUCK_STOP) { + "Ducking packet action must be a ducking action." + } + return PacketSoundInstanceS2C( + action, + key, + emptySound, + SoundSource.MASTER, + entityId, + pos, + 0f, + 1f, + false, + false, + true, + volumeMultiplier, + range, + whitelistSounds, + whitelistSources, + whitelistKeys + ) + } + + private fun writeResourceLocationSet(buf: PacketByteBuf, values: Set) { + buf.writeVarInt(values.size) + values.forEach(buf::writeResourceLocation) + } + + private fun readResourceLocationSet(buf: PacketByteBuf): Set { + val result = LinkedHashSet() + repeat(buf.readVarInt()) { + result.add(buf.readResourceLocation()) + } + return result + } + + private fun writeSoundSourceSet(buf: PacketByteBuf, values: Set) { + buf.writeVarInt(values.size) + values.forEach(buf::writeEnum) + } + + private fun readSoundSourceSet(buf: PacketByteBuf): Set { + val result = LinkedHashSet() + repeat(buf.readVarInt()) { + result.add(buf.readEnum(SoundSource::class.java)) + } + return result + } + + private fun writeStringSet(buf: PacketByteBuf, values: Set) { + buf.writeVarInt(values.size) + values.forEach(buf::writeUtf) + } + + private fun readStringSet(buf: PacketByteBuf): Set { + val result = LinkedHashSet() + repeat(buf.readVarInt()) { + result.add(buf.readUtf()) + } + return result + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt new file mode 100644 index 00000000..6e95568e --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt @@ -0,0 +1,77 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.sounds.SoundSource +import net.minecraft.world.phys.Vec3 + +class PacketSoundLoopS2C( + val key: String, + val sound: ResourceLocation, + val source: SoundSource, + val entityId: Int, + val pos: Vec3, + val volume: Float, + val pitch: Float, + val start: Boolean, + val stopImmediately: Boolean +) { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_loop") + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_loop") + + val CODEC = ForgeStreamCodec.of( + { packet, buf -> + buf.writeUtf(packet.key) + buf.writeResourceLocation(packet.sound) + buf.writeEnum(packet.source) + buf.writeInt(packet.entityId) + buf.writeVec3(packet.pos) + buf.writeFloat(packet.volume) + buf.writeFloat(packet.pitch) + buf.writeBoolean(packet.start) + buf.writeBoolean(packet.stopImmediately) + }, + { buf -> + PacketSoundLoopS2C( + key = buf.readUtf(), + sound = buf.readResourceLocation(), + source = buf.readEnum(SoundSource::class.java), + entityId = buf.readInt(), + pos = buf.readVec3(), + volume = buf.readFloat(), + pitch = buf.readFloat(), + start = buf.readBoolean(), + stopImmediately = buf.readBoolean() + ) + } + ) + + fun start( + key: String, + sound: ResourceLocation, + source: SoundSource, + entityId: Int, + pos: Vec3, + volume: Float, + pitch: Float + ): PacketSoundLoopS2C { + return PacketSoundLoopS2C(key, sound, source, entityId, pos, volume, pitch, true, true) + } + + fun stop(key: String, interrupt: Boolean = true): PacketSoundLoopS2C { + return PacketSoundLoopS2C( + key = key, + sound = ResourceLocation.withDefaultNamespace("empty"), + source = SoundSource.MASTER, + entityId = -1, + pos = Vec3.ZERO, + volume = 0f, + pitch = 1f, + start = false, + stopImmediately = interrupt + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt new file mode 100644 index 00000000..4eb13b50 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt @@ -0,0 +1,320 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectComposition +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectGroupSnapshot +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectRegistry +import net.minecraft.core.BlockPos +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class PacketTerrainEffectGroupS2C { + var operation: Int = REPLACE + var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") + var groupId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + var pipelineId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + var startedAt: Long = 0L + var expiresAt: Long? = null + var sequence: Long = 0L + var revision: Long = 0L + var priority: Int = 0 + var composition: CooTerrainEffectComposition = CooTerrainEffectComposition.REPLACE + var positions: Map = emptyMap() + var uniforms: Map = emptyMap() + + override fun id(): ResourceLocation = ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, + PACKET_ID + ) + + override fun codec(): CommonStreamCodec = CODEC + + override fun onClientReceive(context: cn.coostack.cooparticlesapi.network.packet.api.ClientContext) { + context.client.execute { + when (operation) { + REPLACE -> CooTerrainEffectRegistry.install( + CooTerrainEffectGroupSnapshot( + dimension = dimension, + id = groupId, + pipelineId = pipelineId, + startedAt = startedAt, + expiresAt = expiresAt, + activations = positions, + uniforms = uniforms, + sequence = sequence, + revision = revision, + priority = priority, + composition = composition + ) + ) + APPEND -> CooTerrainEffectRegistry.append(dimension, groupId, revision, positions) + REMOVE_POSITIONS -> CooTerrainEffectRegistry.removePositions( + dimension, + groupId, + revision, + positions.keys + ) + UPDATE_UNIFORMS -> CooTerrainEffectRegistry.updateUniforms( + dimension, + groupId, + revision, + uniforms + ) + REMOVE_GROUP -> CooTerrainEffectRegistry.remove(dimension, groupId, revision) + UPDATE_GROUP_OPTIONS -> CooTerrainEffectRegistry.updateOrdering( + dimension, + groupId, + revision, + priority, + composition + ) + } + } + } + + companion object { + private const val PACKET_ID = "terrain_effect_group_s2c" + private const val REPLACE = 0 + private const val APPEND = 1 + private const val REMOVE_POSITIONS = 2 + private const val UPDATE_UNIFORMS = 3 + private const val REMOVE_GROUP = 4 + private const val UPDATE_GROUP_OPTIONS = 5 + + val CODEC: CommonStreamCodec = CommonStreamCodec.of(::encode, ::decode) + + internal fun replace(snapshot: CooTerrainEffectGroupSnapshot): PacketTerrainEffectGroupS2C { + return PacketTerrainEffectGroupS2C().also { + it.operation = REPLACE + it.dimension = snapshot.dimension + it.groupId = snapshot.id + it.pipelineId = snapshot.pipelineId + it.startedAt = snapshot.startedAt + it.expiresAt = snapshot.expiresAt + it.sequence = snapshot.sequence + it.revision = snapshot.revision + it.priority = snapshot.priority + it.composition = snapshot.composition + it.positions = snapshot.activations + it.uniforms = snapshot.uniforms + } + } + + internal fun updateOrdering( + dimension: ResourceLocation, + groupId: ResourceLocation, + revision: Long, + priority: Int, + composition: CooTerrainEffectComposition + ): PacketTerrainEffectGroupS2C { + return PacketTerrainEffectGroupS2C().also { + it.operation = UPDATE_GROUP_OPTIONS + it.dimension = dimension + it.groupId = groupId + it.revision = revision + it.priority = priority + it.composition = composition + } + } + + internal fun append( + dimension: ResourceLocation, + groupId: ResourceLocation, + revision: Long, + positions: Map + ): PacketTerrainEffectGroupS2C { + return PacketTerrainEffectGroupS2C().also { + it.operation = APPEND + it.dimension = dimension + it.groupId = groupId + it.revision = revision + it.positions = positions + } + } + + internal fun remove( + dimension: ResourceLocation, + groupId: ResourceLocation, + revision: Long + ): PacketTerrainEffectGroupS2C { + return PacketTerrainEffectGroupS2C().also { + it.operation = REMOVE_GROUP + it.dimension = dimension + it.groupId = groupId + it.revision = revision + } + } + + internal fun removePositions( + dimension: ResourceLocation, + groupId: ResourceLocation, + revision: Long, + positions: Set + ): PacketTerrainEffectGroupS2C { + return PacketTerrainEffectGroupS2C().also { + it.operation = REMOVE_POSITIONS + it.dimension = dimension + it.groupId = groupId + it.revision = revision + it.positions = positions.associateWith { 0L } + } + } + + internal fun updateUniforms( + dimension: ResourceLocation, + groupId: ResourceLocation, + revision: Long, + uniforms: Map + ): PacketTerrainEffectGroupS2C { + return PacketTerrainEffectGroupS2C().also { + it.operation = UPDATE_UNIFORMS + it.dimension = dimension + it.groupId = groupId + it.revision = revision + it.uniforms = uniforms + } + } + + private fun encode(buffer: PacketByteBuf, packet: PacketTerrainEffectGroupS2C) { + buffer.writeByte(packet.operation) + buffer.writeResourceLocation(packet.dimension) + buffer.writeResourceLocation(packet.groupId) + buffer.writeVarLong(packet.revision) + when (packet.operation) { + REPLACE -> { + buffer.writeResourceLocation(packet.pipelineId) + buffer.writeVarLong(packet.startedAt) + buffer.writeVarLong(packet.sequence) + buffer.writeVarInt(packet.priority) + buffer.writeVarInt(packet.composition.ordinal) + buffer.writeBoolean(packet.expiresAt != null) + packet.expiresAt?.let(buffer::writeVarLong) + writeUniforms(buffer, packet.uniforms) + writeTimedPositions(buffer, packet.positions) + } + APPEND -> writeTimedPositions(buffer, packet.positions) + REMOVE_POSITIONS -> writePositions(buffer, packet.positions.keys) + UPDATE_UNIFORMS -> writeUniforms(buffer, packet.uniforms) + UPDATE_GROUP_OPTIONS -> { + buffer.writeVarInt(packet.priority) + buffer.writeVarInt(packet.composition.ordinal) + } + } + } + + private fun decode(buffer: PacketByteBuf): PacketTerrainEffectGroupS2C { + val packet = PacketTerrainEffectGroupS2C() + packet.operation = buffer.readUnsignedByte().toInt() + packet.dimension = buffer.readResourceLocation() + packet.groupId = buffer.readResourceLocation() + packet.revision = buffer.readVarLong() + when (packet.operation) { + REPLACE -> { + packet.pipelineId = buffer.readResourceLocation() + packet.startedAt = buffer.readVarLong() + packet.sequence = buffer.readVarLong() + packet.priority = buffer.readVarInt() + packet.composition = CooTerrainEffectComposition.fromWire(buffer.readVarInt()) + packet.expiresAt = if (buffer.readBoolean()) buffer.readVarLong() else null + packet.uniforms = readUniforms(buffer) + packet.positions = readTimedPositions(buffer) + } + APPEND -> packet.positions = readTimedPositions(buffer) + REMOVE_POSITIONS -> packet.positions = readPositions(buffer).associateWith { 0L } + UPDATE_UNIFORMS -> packet.uniforms = readUniforms(buffer) + UPDATE_GROUP_OPTIONS -> { + packet.priority = buffer.readVarInt() + packet.composition = CooTerrainEffectComposition.fromWire(buffer.readVarInt()) + } + } + return packet + } + + private fun writeTimedPositions(buffer: PacketByteBuf, positions: Map) { + val entries = positions.entries.toList() + buffer.writeVarInt(entries.size) + if (entries.isEmpty()) return + val origin = entries.first().key + val activationBase = entries.first().value + buffer.writeBlockPos(origin) + buffer.writeVarLong(activationBase) + entries.forEach { (position, activation) -> + buffer.writeVarInt(zigZag(position.x - origin.x)) + buffer.writeVarInt(zigZag(position.y - origin.y)) + buffer.writeVarInt(zigZag(position.z - origin.z)) + buffer.writeVarLong(zigZag(activation - activationBase)) + } + } + + private fun readTimedPositions(buffer: PacketByteBuf): Map { + val count = buffer.readVarInt() + if (count == 0) return emptyMap() + val origin = buffer.readBlockPos() + val activationBase = buffer.readVarLong() + val result = LinkedHashMap(count) + repeat(count) { + val position = origin.offset( + unZigZag(buffer.readVarInt()), + unZigZag(buffer.readVarInt()), + unZigZag(buffer.readVarInt()) + ) + result[position] = activationBase + unZigZag(buffer.readVarLong()) + } + return result + } + + private fun writePositions(buffer: PacketByteBuf, positions: Collection) { + val entries = positions.toList() + buffer.writeVarInt(entries.size) + if (entries.isEmpty()) return + val origin = entries.first() + buffer.writeBlockPos(origin) + entries.forEach { position -> + buffer.writeVarInt(zigZag(position.x - origin.x)) + buffer.writeVarInt(zigZag(position.y - origin.y)) + buffer.writeVarInt(zigZag(position.z - origin.z)) + } + } + + private fun readPositions(buffer: PacketByteBuf): Set { + val count = buffer.readVarInt() + if (count == 0) return emptySet() + val origin = buffer.readBlockPos() + return buildSet(count) { + repeat(count) { + add( + origin.offset( + unZigZag(buffer.readVarInt()), + unZigZag(buffer.readVarInt()), + unZigZag(buffer.readVarInt()) + ) + ) + } + } + } + + private fun writeUniforms(buffer: PacketByteBuf, uniforms: Map) { + buffer.writeVarInt(uniforms.size) + uniforms.forEach { (name, value) -> + buffer.writeUtf(name) + CooUniformValue.STREAM_CODEC.encode(buffer, value) + } + } + + private fun readUniforms(buffer: PacketByteBuf): Map { + val count = buffer.readVarInt() + val result = LinkedHashMap(count) + repeat(count) { + val name = buffer.readUtf() + result[name] = CooUniformValue.STREAM_CODEC.decode(buffer) + } + return result + } + + private fun zigZag(value: Int): Int = (value shl 1) xor (value shr 31) + private fun unZigZag(value: Int): Int = (value ushr 1) xor -(value and 1) + private fun zigZag(value: Long): Long = (value shl 1) xor (value shr 63) + private fun unZigZag(value: Long): Long = (value ushr 1) xor -(value and 1L) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt new file mode 100644 index 00000000..5d24e2d2 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt @@ -0,0 +1,175 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectComposition +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainMappingInstance +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainMappingRegion +import cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainMappingRegistry +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +class PacketTerrainMappingS2C { + var operation: Int = REPLACE + var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") + var instanceId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + var mappingId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + var pipelineId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + var region: CooTerrainMappingRegion? = null + var startedAt: Long = 0L + var expiresAt: Long? = null + var sequence: Long = 0L + var revision: Long = 0L + var pausedAt: Long? = null + var priority: Int = 0 + var composition: CooTerrainEffectComposition = CooTerrainEffectComposition.REPLACE + var uniforms: Map = emptyMap() + + override fun id(): ResourceLocation = ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, + PACKET_ID + ) + + override fun codec(): CommonStreamCodec = CODEC + + override fun onClientReceive(context: cn.coostack.cooparticlesapi.network.packet.api.ClientContext) { + context.client.execute { + when (operation) { + REPLACE -> CooTerrainMappingRegistry.install(toInstance()) + UPDATE_UNIFORMS -> CooTerrainMappingRegistry.updateUniforms( + dimension, + instanceId, + revision, + uniforms + ) + REMOVE -> CooTerrainMappingRegistry.remove(dimension, instanceId, revision) + } + } + } + + private fun toInstance(): CooTerrainMappingInstance = CooTerrainMappingInstance( + instanceId, + mappingId, + dimension, + requireNotNull(region) { "Terrain mapping replace packet has no region" }, + uniforms, + priority, + composition, + startedAt, + expiresAt, + sequence, + revision, + pausedAt + ) + + companion object { + private const val PACKET_ID = "terrain_mapping_s2c" + private const val REPLACE = 0 + private const val UPDATE_UNIFORMS = 1 + private const val REMOVE = 2 + val CODEC: CommonStreamCodec = CommonStreamCodec.of(::encode, ::decode) + + internal fun replace(instance: CooTerrainMappingInstance, pipelineId: ResourceLocation): PacketTerrainMappingS2C = + PacketTerrainMappingS2C().also { + it.operation = REPLACE + it.dimension = instance.dimension + it.instanceId = instance.instanceId + it.mappingId = instance.mappingId + it.pipelineId = pipelineId + it.region = instance.region + it.startedAt = instance.startedAt + it.expiresAt = instance.expiresAt + it.sequence = instance.sequence + it.revision = instance.revision + it.pausedAt = instance.pausedAt + it.priority = instance.priority + it.composition = instance.composition + it.uniforms = instance.uniforms + } + + internal fun updateUniforms(instance: CooTerrainMappingInstance): PacketTerrainMappingS2C = + PacketTerrainMappingS2C().also { + it.operation = UPDATE_UNIFORMS + it.dimension = instance.dimension + it.instanceId = instance.instanceId + it.revision = instance.revision + it.uniforms = instance.uniforms + } + + internal fun remove( + dimension: ResourceLocation, + instanceId: ResourceLocation, + revision: Long + ): PacketTerrainMappingS2C = PacketTerrainMappingS2C().also { + it.operation = REMOVE + it.dimension = dimension + it.instanceId = instanceId + it.revision = revision + } + + private fun encode(buffer: PacketByteBuf, packet: PacketTerrainMappingS2C) { + buffer.writeByte(packet.operation) + buffer.writeResourceLocation(packet.dimension) + buffer.writeResourceLocation(packet.instanceId) + buffer.writeVarLong(packet.revision) + when (packet.operation) { + REPLACE -> { + buffer.writeResourceLocation(packet.mappingId) + buffer.writeResourceLocation(packet.pipelineId) + buffer.writeVarLong(packet.startedAt) + buffer.writeVarLong(packet.sequence) + buffer.writeVarInt(packet.priority) + buffer.writeVarInt(packet.composition.ordinal) + buffer.writeBoolean(packet.expiresAt != null) + packet.expiresAt?.let(buffer::writeVarLong) + buffer.writeBoolean(packet.pausedAt != null) + packet.pausedAt?.let(buffer::writeVarLong) + packet.region?.encode(buffer) ?: error("Terrain mapping replace packet has no region") + writeUniforms(buffer, packet.uniforms) + } + UPDATE_UNIFORMS -> writeUniforms(buffer, packet.uniforms) + } + } + + private fun decode(buffer: PacketByteBuf): PacketTerrainMappingS2C { + val packet = PacketTerrainMappingS2C() + packet.operation = buffer.readUnsignedByte().toInt() + packet.dimension = buffer.readResourceLocation() + packet.instanceId = buffer.readResourceLocation() + packet.revision = buffer.readVarLong() + when (packet.operation) { + REPLACE -> { + packet.mappingId = buffer.readResourceLocation() + packet.pipelineId = buffer.readResourceLocation() + packet.startedAt = buffer.readVarLong() + packet.sequence = buffer.readVarLong() + packet.priority = buffer.readVarInt() + packet.composition = CooTerrainEffectComposition.fromWire(buffer.readVarInt()) + packet.expiresAt = if (buffer.readBoolean()) buffer.readVarLong() else null + packet.pausedAt = if (buffer.readBoolean()) buffer.readVarLong() else null + packet.region = CooTerrainMappingRegion.decode(buffer) + packet.uniforms = readUniforms(buffer) + } + UPDATE_UNIFORMS -> packet.uniforms = readUniforms(buffer) + } + return packet + } + + private fun writeUniforms(buffer: PacketByteBuf, uniforms: Map) { + buffer.writeVarInt(uniforms.size) + uniforms.forEach { (name, value) -> + buffer.writeUtf(name) + CooUniformValue.STREAM_CODEC.encode(buffer, value) + } + } + + private fun readUniforms(buffer: PacketByteBuf): Map { + val count = buffer.readVarInt() + val result = LinkedHashMap(count) + repeat(count) { + result[buffer.readUtf()] = CooUniformValue.STREAM_CODEC.decode(buffer) + } + return result + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt new file mode 100644 index 00000000..ee2b9e6f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoParticleComposition(position: Vec3, world: Level? = null) : ParticleComposition(position, world) { + constructor(world: Level?) : this(Vec3.ZERO, world) + constructor(world: Level?, pos: Vec3) : this(pos, world) + + override fun getCodec(): CommonStreamCodec { + return ParticleCompositionRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt new file mode 100644 index 00000000..06a55930 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoSequencedParticleComposition(position: Vec3, world: Level? = null) : SequencedParticleComposition(position, world) { + constructor(world: Level?) : this(Vec3.ZERO, world) + constructor(world: Level?, pos: Vec3) : this(pos, world) + + override fun getCodec(): CommonStreamCodec { + return ParticleCompositionRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt new file mode 100644 index 00000000..3838dbfe --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt @@ -0,0 +1,993 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper +import cn.coostack.cooparticlesapi.api.NetworkDirtyMarkable +import cn.coostack.cooparticlesapi.extend.asRelative +import cn.coostack.cooparticlesapi.api.controler.server.ServerControler +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.Tickable +import cn.coostack.cooparticlesapi.cparticle.CParticleSystem +import cn.coostack.cooparticlesapi.cparticle.CParticleSystemManager +import cn.coostack.cooparticlesapi.cparticle.CParticleSystemMode +import cn.coostack.cooparticlesapi.cparticle.CParticleRenderLayer +import cn.coostack.cooparticlesapi.cparticle.CParticleCurve +import cn.coostack.cooparticlesapi.cparticle.CParticleTransitionMode +import cn.coostack.cooparticlesapi.cparticle.CParticleTextureBindingKey +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleDisplayer +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.particles.control.ControlParticleManager +import cn.coostack.cooparticlesapi.particles.control.ParticleControler +import cn.coostack.cooparticlesapi.particles.control.RemoveReason +import cn.coostack.cooparticlesapi.network.particle.style.ParticleGroupStyle +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CParticleCompositionAlphaHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionStatusHelper +import net.minecraft.client.Minecraft +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import org.joml.Matrix4f +import org.joml.Quaternionf +import org.joml.Vector3f +import org.joml.Vector3fc +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.PI + +abstract class ParticleComposition : ServerControler, + Controlable, Tickable, NetworkDirtyMarkable { + companion object { + @JvmStatic + fun encodeBase(data: ParticleComposition, buf: PacketByteBuf) { + buf.writeUUID(data.controlUUID) + buf.writeDouble(data.visibleRange) + buf.writeBoolean(data.canceled) + buf.writeVec3(data.position) + buf.writeVec3(data.axis.toVector()) + buf.writeDouble(data.scale) + buf.writeInt(data.status.displayStatus) + buf.writeInt(data.status.closedInternal) + buf.writeInt(data.status.current) + } + + @JvmStatic + fun decodeBase(instance: ParticleComposition, buf: PacketByteBuf) { + instance.apply { + controlUUID = buf.readUUID() + visibleRange = buf.readDouble() + canceled = buf.readBoolean() + position = buf.readVec3() + axis = buf.readVec3().asRelative() + scale = buf.readDouble() + status.setStatus(buf.readInt()) + status.closedInternal = buf.readInt() + status.updateCurrent(buf.readInt()) + } + } + } + + constructor(pos: Vec3, world: Level?) { + this.position = pos + this.world = world + } + + constructor(world: Level) { + this.world = world + } + + constructor(world: Level, pos: Vec3) { + this.world = world + this.position = pos + } + + var position: Vec3 = Vec3.ZERO + protected set + var world: Level? = null + internal set + + var visibleRange = 256.0 + set(value) { + if (field == value) return + field = value + markNetworkStateDirty() + } + + var scale = 1.0 + private set + var client = false + protected set + + var displayed = false + protected set + + var canceled = false + + var controlUUID = UUID.randomUUID() + + var axis = RelativeLocation.yAxis() + + var roll = 0.0 + + val particles = ConcurrentHashMap>() + val controlerTicks = HashSet>() + val particleLocations = ConcurrentHashMap, RelativeLocation>() + val status = CompositionStatusHelper() + val particleDefaultLength = ConcurrentHashMap() + private val particleDefaultLocations = ConcurrentHashMap() + internal val invokeQueue = ArrayList Unit>() + internal val postInvokeQueue = ArrayList Unit>() + protected val particleRotatedLocations = ArrayList() + private val managedCParticleSystems = LinkedHashSet() + private val referencedCParticleSystems = LinkedHashMap() + private val cParticleContainers = LinkedHashSet>() + private val cParticleSystemConfigurations = + LinkedHashMap Unit>>() + private val cParticleLinearTransform = Matrix4f() + private val cParticleRenderTransform = Matrix4f() + private var cParticleCapacityHint = 1 + private var managedCParticleCount = 0 + private var gpuTransformActive = false + private var cParticleAppliedScale = 1.0 + private var cParticleScaleCollapsed = false + private var networkStateDirty = true + private var networkFullDirty = true + private var lastNetworkStatus = status.displayStatus + private var lastNetworkStatusInterval = status.closedInternal + + override fun markDirty() { + if (world?.isClientSide != true) { + networkFullDirty = true + networkStateDirty = true + } + } + + internal fun markNetworkStateDirty() { + if (world?.isClientSide != true) { + networkStateDirty = true + } + } + + internal fun consumeNetworkFullDirty(): Boolean { + val dirty = networkFullDirty + networkFullDirty = false + return dirty + } + + internal fun hasNetworkFullDirty(): Boolean = networkFullDirty + + internal fun hasNetworkStateDirty(): Boolean { + updateNetworkStatusDirty() + return networkStateDirty + } + + internal fun consumeNetworkStateDirty(): Boolean { + updateNetworkStatusDirty() + val dirty = networkStateDirty + networkStateDirty = false + return dirty + } + + private fun updateNetworkStatusDirty() { + if (lastNetworkStatus != status.displayStatus || + lastNetworkStatusInterval != status.closedInternal + ) { + networkStateDirty = true + lastNetworkStatus = status.displayStatus + lastNetworkStatusInterval = status.closedInternal + } + } + + internal fun applyRemoteState( + position: Vec3, + visibleRange: Double, + scale: Double, + displayStatus: Int, + closedInterval: Int, + current: Int, + ) { + this.visibleRange = visibleRange + if (this.position != position) teleportTo(position) + if (this.scale != scale) scale(scale) + status.setStatus(displayStatus) + status.closedInternal = closedInterval + status.updateCurrent(current) + } + + abstract fun getCodec(): CommonStreamCodec + + abstract fun getParticles(): Map + + abstract fun onDisplay() + + fun setDisabledInterval(interval: Int): ParticleComposition { + this.status.closedInternal = interval + markNetworkStateDirty() + return this + } + + fun configureCParticleSystem(configure: CParticleSystem.() -> Unit): ParticleComposition { + return setCParticleSystemConfiguration(null, configure) + } + + fun configureCParticleSystem( + layer: CParticleRenderLayer, + configure: CParticleSystem.() -> Unit, + ): ParticleComposition { + return setCParticleSystemConfiguration(layer, configure) + } + + private fun setCParticleSystemConfiguration( + layer: CParticleRenderLayer?, + configure: CParticleSystem.() -> Unit, + ): ParticleComposition { + cParticleSystemConfigurations.getOrPut(layer) { ArrayList() }.add(configure) + managedCParticleSystems.forEach { system -> + if (layer == null || system.layer == layer) configure(system) + } + return this + } + + fun getCParticleSystem(layer: CParticleRenderLayer): CParticleSystem? { + return managedCParticleSystems.firstOrNull { !it.released && it.layer == layer } + } + + fun getCParticleSystems(): List { + removeReleasedCParticleSystems() + return referencedCParticleSystems.keys.toList() + } + + internal fun getCParticleContainers(): List> = + cParticleContainers.toList() + + protected fun registerCParticleNode(controler: Controlable<*>) { + when (controler) { + is CParticleControlable -> { + removeReleasedCParticleSystems() + referencedCParticleSystems.merge(controler.system, 1, Int::plus) + controler.system.visibleRange = Double.MAX_VALUE + if (controler.system in managedCParticleSystems) { + managedCParticleCount++ + } + } + + is ParticleComposition, + is ParticleGroupStyle, + is ControlableParticleGroup -> cParticleContainers.add(controler) + } + } + + protected fun unregisterCParticleNode(controler: Controlable<*>) { + when (controler) { + is CParticleControlable -> { + val count = referencedCParticleSystems[controler.system] ?: return + if (count <= 1) referencedCParticleSystems.remove(controler.system) + else referencedCParticleSystems[controler.system] = count - 1 + if (controler.system in managedCParticleSystems) { + managedCParticleCount = (managedCParticleCount - 1).coerceAtLeast(0) + } + } + + is ParticleComposition, + is ParticleGroupStyle, + is ControlableParticleGroup -> cParticleContainers.remove(controler) + } + } + + private fun removeReleasedCParticleSystems() { + managedCParticleSystems.removeIf { it.released } + referencedCParticleSystems.keys.removeIf { it.released } + managedCParticleCount = referencedCParticleSystems.entries.sumOf { (system, count) -> + if (system in managedCParticleSystems) count else 0 + } + } + + @JvmOverloads + fun playCParticleVisualTransition( + durationTicks: Float, + alphaCurve: CParticleCurve? = null, + scaleCurve: CParticleCurve? = null, + colorFrom: Vector3fc? = null, + colorTo: Vector3fc? = null, + mode: CParticleTransitionMode = CParticleTransitionMode.HOLD_END, + ): ParticleComposition { + return playCParticleVisualTransitionInternal( + durationTicks, + alphaCurve, + scaleCurve, + colorFrom, + colorTo, + mode, + restart = false, + ) + } + + @JvmOverloads + fun playCParticleVisualTransition( + durationTicks: Float, + restart: Boolean, + alphaCurve: CParticleCurve? = null, + scaleCurve: CParticleCurve? = null, + colorFrom: Vector3fc? = null, + colorTo: Vector3fc? = null, + mode: CParticleTransitionMode = CParticleTransitionMode.HOLD_END, + ): ParticleComposition { + return playCParticleVisualTransitionInternal( + durationTicks, + alphaCurve, + scaleCurve, + colorFrom, + colorTo, + mode, + restart, + ) + } + + private fun playCParticleVisualTransitionInternal( + durationTicks: Float, + alphaCurve: CParticleCurve?, + scaleCurve: CParticleCurve?, + colorFrom: Vector3fc?, + colorTo: Vector3fc?, + mode: CParticleTransitionMode, + restart: Boolean, + ): ParticleComposition { + getCParticleSystems().forEach { system -> + system.playVisualTransition( + durationTicks = durationTicks, + restart = restart, + alphaCurve = alphaCurve, + scaleCurve = scaleCurve, + colorFrom = colorFrom, + colorTo = colorTo, + mode = mode, + ) + } + return this + } + + @JvmOverloads + fun stopCParticleVisualTransition(reset: Boolean = false): ParticleComposition { + getCParticleSystems().forEach { it.stopVisualTransition(reset) } + return this + } + + @JvmOverloads + fun playCParticleAlphaTransition( + durationTicks: Float, + alphaCurve: CParticleCurve, + mode: CParticleTransitionMode = CParticleTransitionMode.HOLD_END, + restart: Boolean = false, + ): ParticleComposition { + return CParticleCompositionAlphaHelper.play( + composition = this, + durationTicks = durationTicks, + alphaCurve = alphaCurve, + mode = mode, + restart = restart, + ) + } + + fun stopCParticleAlphaTransition(reset: Boolean = false): ParticleComposition { + return CParticleCompositionAlphaHelper.stop(this, reset) + } + + open fun beforeDisplay(map: Map) {} + + override fun tick() { + if (canceled || !displayed) { + return + } + if (client) { + Minecraft.getInstance().player?.let { + if (it.position().distanceTo(position) > visibleRange) { + remove() + return + } + } + } + + invokeQueue.forEach { it() } + val tickIterator = controlerTicks.iterator() + while (tickIterator.hasNext()) { + val controler = tickIterator.next() + if (controler is CParticleControlable && !controler.valid) { + tickIterator.remove() + continue + } + controler.tick() + if (controler is CParticleControlable && !controler.valid) { + tickIterator.remove() + } + } + postInvokeQueue.forEach { it() } + } + + open fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale can not be less than zero") + return + } + if (scale == new) return + scale = new + markNetworkStateDirty() + if (displayed) { + if (gpuTransformActive) { + applyGpuScale(new) + } else { + toggleScaleDisplayed() + } + } + if (!canceled) { + return + } + } + + open fun preRotateTo(map: Map, to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + map.values.toList(), to, axis + ) + this.axis.copyFrom(to) + } + + open fun preRotateAsAxis(map: Map, axis: RelativeLocation, angle: Double) { + Math3DUtil.rotateAsAxis( + map.values.toList(), axis, angle + ) + this.axis.copyFrom(axis) + } + + open fun preRotateAsAxis(map: Map, angle: Double) { + Math3DUtil.rotateAsAxis( + map.values.toList(), axis, angle + ) + } + + protected open fun toggleScaleDisplayed() { + if (!displayed) { + return + } + for (it in particleLocations) { + applyScale(it.key.controlUUID(), it.value) + } + toggleRelative() + } + + open fun update(other: ParticleComposition) { + val newAxis = other.axis.clone() + this.visibleRange = other.visibleRange + if (this.position != other.position) { + teleportTo(other.position) + } + this.canceled = other.canceled + this.controlUUID = other.controlUUID + if (this.scale != other.scale) { + scale(other.scale) + } + if (!client || !displayed) { + this.axis.copyFrom(newAxis) + } + this.status.setStatus(other.status.displayStatus) + this.status.closedInternal = other.status.closedInternal + this.status.updateCurrent(other.status.current) + CodecHelper.updateFields(this, other) + } + + override fun addPreTickAction(action: ParticleComposition.() -> Unit): ParticleComposition { + invokeQueue.add(action) + return this + } + + override fun addPreTickActionPost(action: ParticleComposition.() -> Unit): ParticleComposition { + postInvokeQueue.add(action) + return this + } + + open fun clear(cancel: Boolean) { + particles.forEach { + it.value.remove() + } + resetGpuTransformState() + controlerTicks.clear() + particles.clear() + particleLocations.clear() + particleRotatedLocations.clear() + particleDefaultLength.clear() + particleDefaultLocations.clear() + this.canceled = cancel + if (cancel) { + displayed = false + ParticleCompositionManager.setClientLoaded(this, false) + } + } + + internal fun resetLifecycleForSpawn() { + ParticleCompositionManager.setClientLoaded(this, false) + canceled = false + displayed = false + networkStateDirty = true + networkFullDirty = true + lastNetworkStatus = status.displayStatus + lastNetworkStatusInterval = status.closedInternal + } + + open fun display() { + if (displayed) { + return + } + displayed = true + this.client = world!!.isClientSide + if (client) { + ParticleCompositionManager.setClientLoaded(this, true) + } + flush() + status.loadControler(this) + status.initHelper() + if (!client) { + onDisplay() + return + } + onDisplay() + } + + fun toggleScale(locations: Map) { + if (canceled) { + return + } + locations.forEach { (data, location) -> + particleDefaultLength.putIfAbsent(data.uuid, location.length()) + particleDefaultLocations.putIfAbsent(data.uuid, location.clone()) + } + locations.forEach { (data, location) -> + applyScale(data.uuid, location) + } + } + + protected fun applyScale(uuid: UUID, location: RelativeLocation) { + val defaultLength = particleDefaultLength[uuid] ?: return + if (defaultLength <= 0.0) return + if (location.length() <= 0.000000000001) { + location.copyFrom(particleDefaultLocations[uuid] ?: return) + } + location.multiply(defaultLength * scale / location.length()) + } + + open fun flush() { + if (particles.isNotEmpty()) { + clear(false) + } + displayParticles() + } + + open fun toggleRelative() { + if (!client) { + return + } + if (gpuTransformActive) { + syncGpuTransform() + return + } + val staleControls = ArrayList>() + val iterator = particleLocations.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val particle = entry.key + val rel = entry.value + if (particle is ParticleControler && !particle.isBound) { + staleControls.add(particle) + continue + } + try { + particle.teleportTo( + position.add(rel.x, rel.y, rel.z) + ) + } catch (error: IllegalStateException) { + if (particle is ParticleControler && !particle.isBound) { + staleControls.add(particle) + continue + } + throw error + } + } + staleControls.forEach { removeDisplayedControl(it) } + } + + private fun removeDisplayedControl(control: Controlable<*>) { + unregisterCParticleNode(control) + control.remove(RemoveReason.QUEUE) + if (control is Tickable<*>) { + controlerTicks.remove(control) + } + particleLocations.remove(control) + particles.remove(control.controlUUID()) + particleDefaultLength.remove(control.controlUUID()) + particleDefaultLocations.remove(control.controlUUID()) + particleRotatedLocations.clear() + particleLocations.values.forEach { particleRotatedLocations.add(it) } + } + + override fun teleportTo(to: Vec3) { + if (position == to) return + position = to + markNetworkStateDirty() + toggleRelative() + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + override fun rotateToPoint(to: RelativeLocation) { + if (!client) { + axis.copyFrom(to) + ParticleCompositionManager.sendRotate(this, to, 0.0) + return + } + if (gpuTransformActive) { + applyGpuRotationTo(axis, to, 0.0) + axis.copyFrom(to) + return + } + Math3DUtil.rotatePointsToPoint( + particleRotatedLocations, to, axis + ) + axis.copyFrom(to) + toggleRelative() + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + this.roll += radian + if (this.roll >= 2 * PI) { + this.roll -= 2 * PI + } else if (this.roll <= -2 * PI) { + this.roll += 2 * PI + } + + if (!client) { + axis.copyFrom(to) + ParticleCompositionManager.sendRotate(this, to, radian) + return + } + if (gpuTransformActive) { + applyGpuRotationTo(axis, to, radian) + axis.copyFrom(to) + return + } + Math3DUtil.rotateToWithRoll( + particleRotatedLocations, axis, to, radian + ) + axis.copyFrom(to) + + toggleRelative() + } + + override fun rotateAsAxis(radian: Double) { + this.roll += radian + if (this.roll >= 2 * PI) { + this.roll -= 2 * PI + } else if (this.roll <= -2 * PI) { + this.roll += 2 * PI + } + if (!client) { + ParticleCompositionManager.sendRotate(this, null, radian) + return + } + if (gpuTransformActive) { + applyGpuAxisRotation(axis, radian) + return + } + Math3DUtil.rotateAsAxis( + particleRotatedLocations, axis, radian + ) + toggleRelative() + } + + override fun remove() { + clear(true) + } + + override fun remove(reason: RemoveReason) { + remove() + } + + override fun spawn(world: Level, pos: Vec3) { + this.world = world + this.position = pos + ParticleCompositionManager.spawn(this) + } + + override fun getValue(): ParticleComposition { + return this + } + + override fun controlUUID(): UUID { + return controlUUID + } + + override fun getControlObject(): ParticleComposition { + return this + } + + override fun isValid(): Boolean { + return !canceled + } + + fun setPositionWithoutToggle(pos: Vec3) { + this.position = pos + } + + internal fun applyRemoteRotation(to: RelativeLocation?, radian: Double) { + if (!client || !displayed) { + to?.let { axis.copyFrom(it) } + return + } + if (gpuTransformActive) { + if (to == null) { + applyGpuAxisRotation(axis, radian) + } else { + applyGpuRotationTo(axis, to, radian) + axis.copyFrom(to) + } + return + } + if (particleRotatedLocations.isEmpty()) { + to?.let { axis.copyFrom(it) } + return + } + if (to == null) { + Math3DUtil.rotateAsAxis(particleRotatedLocations, axis, radian) + } else { + Math3DUtil.rotateToWithRoll(particleRotatedLocations, axis, to, radian) + axis.copyFrom(to) + } + toggleRelative() + } + + protected open fun displayEntry(data: CompositionData, pos: RelativeLocation) { + val uuid = data.uuid + val displayer = data.displayerBuilder(uuid) + val managedCParticleSystem = if (displayer is CParticleDisplayer) { + data.cParticleHandlers.forEach(displayer::applyParticleInit) + bindManagedSystem(displayer) + } else null + if (displayer is ParticleDisplayer.SingleParticleDisplayer) { + val controler = ControlParticleManager.createControl(uuid) + controler.applyInitializedAction { + for (function in data.singleParticleHandlers) { + function(this) + } + } + } + val toPos = resolveCParticleSpawnPosition(pos, managedCParticleSystem) + val clientWorld = world as ClientLevel + val controler = if (displayer is CParticleDisplayer) { + displayer.display( + toPos, + clientWorld, + resolveCParticleStoragePosition(pos, managedCParticleSystem), + ) + } else { + displayer.display(toPos, clientWorld) + } ?: let { + CooParticlesConstants.logger.error("display生成了null 错误target类型 ${displayer::class.java.name}") + return + } + if (controler is ParticleControler) { + data.particleControlerHandlers.forEach { handler -> + handler(controler) + } + } + if (controler is CParticleControlable && controler.hasTickActions) { + controlerTicks.add(controler) + } else if (controler is Tickable<*> && controler !is CParticleControlable) { + controlerTicks.add(controler) + } + trackDisplayedParticleLocation(pos) + particles[uuid] = controler + particleLocations[controler] = pos + } + + protected open fun displayParticles() { + if (!client) { + return + } + val locations = getParticles() + prepareGpuComposition(locations.size) + beforeDisplay(locations) + Math3DUtil.rotatePointsToPoint(locations.values.toList(), axis, RelativeLocation.yAxis()) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, roll) + toggleScale(locations) + locations.forEach { + displayEntry(it.key, it.value) + } + refreshGpuTransformMode() + } + + protected fun prepareGpuComposition(particleCount: Int) { + cParticleCapacityHint = particleCount.coerceAtLeast(1) + cParticleAppliedScale = if (scale > 0.0000001) scale else 1.0 + cParticleScaleCollapsed = scale <= 0.0000001 + } + + protected open fun trackDisplayedParticleLocation(pos: RelativeLocation) { + particleRotatedLocations.add(pos) + } + + private fun bindManagedSystem(displayer: CParticleDisplayer): CParticleSystem? { + if (displayer.hasBoundSystem) return null + val layerName = displayer.layer.name.lowercase() + val name = "composition/$controlUUID/$layerName" + val resolvedTextures = displayer.resolveTexturesAt(position) + if (!resolvedTextures.isValid) return null + val bindingKey = resolvedTextures.base.bindingKey + val maskBindingKey = resolvedTextures.mask?.bindingKey + val existing = CParticleSystemManager.getSystem( + name, + CParticleSystemMode.SCRIPTED, + displayer.layer, + bindingKey, + maskBindingKey, + ) + if (existing != null && existing.capacity < cParticleCapacityHint) { + CParticleSystemManager.removeSystem( + name, + CParticleSystemMode.SCRIPTED, + displayer.layer, + bindingKey, + maskBindingKey, + ) + } + val target = CParticleSystemManager.getOrCreateSystem( + name, + cParticleCapacityHint, + displayer.layer, + CParticleSystemMode.SCRIPTED, + bindingKey, + autoReleaseWhenEmpty = true, + maskTextureBindingKey = maskBindingKey, + ) + if (!displayer.bindSystemIfAbsent(target)) return null + target.setOriginIfEmpty(position) + target.visibleRange = Double.MAX_VALUE + cParticleSystemConfigurations[null]?.forEach { it(target) } + cParticleSystemConfigurations[displayer.layer]?.forEach { it(target) } + managedCParticleSystems.add(target) + if (gpuTransformActive) { + syncGpuTransform() + } + return target + } + + internal fun resolveCParticleSpawnPosition( + pos: RelativeLocation, + managedSystem: CParticleSystem?, + ): Vec3 { + if (!gpuTransformActive || managedSystem == null) { + return position.add(pos.x, pos.y, pos.z) + } + val transformed = managedSystem.groupTransform.transformPosition( + Vector3f(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat()) + ) + return managedSystem.origin.add( + transformed.x.toDouble(), + transformed.y.toDouble(), + transformed.z.toDouble(), + ) + } + + internal fun resolveCParticleStoragePosition( + pos: RelativeLocation, + managedSystem: CParticleSystem?, + ): Vec3? { + val system = managedSystem?.takeIf { gpuTransformActive } ?: return null + return system.origin.add(pos.x, pos.y, pos.z) + } + + private fun applyGpuAxisRotation(rotationAxis: RelativeLocation, radian: Float) { + val axisVector = normalizedAxis(rotationAxis) + val delta = Matrix4f().rotate(radian.toDouble(), axisVector) + delta.mul(cParticleLinearTransform, cParticleLinearTransform) + syncGpuTransform() + } + + private fun applyGpuRotationTo(from: RelativeLocation, to: RelativeLocation, radian: Double) { + val fromVector = normalizedAxis(from) + val normalizedFrom = RelativeLocation.of(fromVector) + val normalizedTo = RelativeLocation.of(normalizedAxis(to)) + val alignRotation = Quaternionf() + .rotateY(-Math3DUtil.getYawFromLocation(normalizedTo).toFloat()) + .rotateX(-Math3DUtil.getPitchFromLocation(normalizedTo).toFloat()) + .mul( + Quaternionf() + .rotateY(Math3DUtil.getYawFromLocation(normalizedFrom).toFloat()) + .rotateLocalX(Math3DUtil.getPitchFromLocation(normalizedFrom).toFloat()) + ) + if (radian != 0.0) { + alignRotation.rotateAxis(radian.toFloat(), fromVector) + } + val align = Matrix4f().rotation(alignRotation) + align.mul(cParticleLinearTransform, cParticleLinearTransform) + syncGpuTransform() + } + + private fun applyGpuScale(newScale: Double) { + if (newScale <= 0.0000001) { + cParticleScaleCollapsed = true + syncGpuTransform() + return + } + val factor = (newScale / cParticleAppliedScale).toFloat() + Matrix4f().scaling(factor).mul(cParticleLinearTransform, cParticleLinearTransform) + cParticleAppliedScale = newScale + cParticleScaleCollapsed = false + syncGpuTransform() + } + + protected fun refreshGpuTransformMode() { + val wasActive = gpuTransformActive + gpuTransformActive = managedCParticleCount > 0 && + managedCParticleCount == particles.size && + controlerTicks.none { it is CParticleControlable } + if (gpuTransformActive) { + syncGpuTransform() + if (!wasActive) { + managedCParticleSystems.forEach { it.snapGroupTransform() } + } + } + } + + private fun syncGpuTransform() { + val linear = if (cParticleScaleCollapsed) { + cParticleRenderTransform.set(cParticleLinearTransform) + .m00(0F).m01(0F).m02(0F) + .m10(0F).m11(0F).m12(0F) + .m20(0F).m21(0F).m22(0F) + } else { + cParticleLinearTransform + } + managedCParticleSystems.forEach { system -> + system.groupTransform + .set(linear) + .m30((position.x - system.origin.x).toFloat()) + .m31((position.y - system.origin.y).toFloat()) + .m32((position.z - system.origin.z).toFloat()) + } + } + + private fun resetGpuTransformState() { + managedCParticleSystems.forEach { it.groupTransform.identity() } + managedCParticleSystems.clear() + referencedCParticleSystems.clear() + cParticleContainers.clear() + cParticleLinearTransform.identity() + cParticleRenderTransform.identity() + cParticleCapacityHint = 1 + managedCParticleCount = 0 + gpuTransformActive = false + cParticleAppliedScale = 1.0 + cParticleScaleCollapsed = false + } + + private fun normalizedAxis(value: RelativeLocation): Vector3f { + val result = Vector3f(value.x.toFloat(), value.y.toFloat(), value.z.toFloat()) + return if (result.lengthSquared() > 0.000000000001F) result.normalize() else result.set(0F, 1F, 0F) + } + + open fun clone(): ParticleComposition { + val new = runCatching { + this::class.java.getDeclaredConstructor(Vec3::class.java, Level::class.java) + .apply { isAccessible = true } + .newInstance(Vec3.ZERO, null) + }.getOrNull() ?: this::class.java.getDeclaredConstructor() + .apply { isAccessible = true } + .newInstance() + new.world = world + new.update(this) + return new + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt new file mode 100644 index 00000000..51b20c74 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt @@ -0,0 +1,131 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +class ParticleShapeComposition(uuid: UUID) : ParticleComposition(Vec3.ZERO, null) { + init { + this.controlUUID = uuid + visibleRange = Double.MAX_VALUE + } + + private val points = ArrayList CompositionData>>() + private val invokes = ArrayList Unit>() + private val beforeInvokes = + ArrayList) -> Unit>() + + var scaleHelper: ScaleHelper? = null + private set + var spawnAge = 0 + var scalePreTick = false + private set + var scaleReversed = false + var reversedClean = true + + fun loadScaleHelper(min: Double, max: Double, scalingTick: Int): ParticleShapeComposition { + scaleHelper = CompositionScaleHelper(min, max, scalingTick) + .apply { + loadControler(this@ParticleShapeComposition) + } + scalePreTick = true + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): ParticleShapeComposition { + scaleHelper = CompositionBezierScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + fun applyDisplayAction(action: ParticleShapeComposition.() -> Unit): ParticleShapeComposition { + invokes.add(action) + return this + } + + fun applyBeforeDisplayAction(action: ParticleShapeComposition.(Map) -> Unit): ParticleShapeComposition { + beforeInvokes.add(action) + return this + } + + fun applyPoint( + point: RelativeLocation, + dataSupplier: (RelativeLocation) -> CompositionData + ): ParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to dataSupplier) + return this + } + + fun applyBuilder( + builder: PointsBuilder, + dataSupplier: (RelativeLocation) -> CompositionData + ): ParticleShapeComposition { + points.add(builder to dataSupplier) + return this + } + + fun setReversedScaleOnDisableStatus(status: StatusHelper): ParticleShapeComposition { + addPreTickAction { + if (status.displayStatus == 2) { + scaleReversed = true + } + } + return this + } + + fun setReversedScaleOnCompositionStatus(composition: ParticleComposition): ParticleShapeComposition { + addPreTickAction { + if (composition.status.getCurrentStatus() == StatusHelper.Status.DISABLE) { + scaleReversed = true + } + } + return this + } + + override fun getCodec(): CommonStreamCodec { + throw NotImplementedError("此类只作为客户端嵌套使用, 不能单独生成! ") + } + + override fun getParticles(): Map { + val res = HashMap() + points.forEach { + res.putAll( + it.first + .createWithCompositionData { rel -> + it.second(rel) + } + ) + } + return res + } + + override fun beforeDisplay(map: Map) { + super.beforeDisplay(map) + beforeInvokes.forEach { it(map) } + } + + override fun onDisplay() { + invokes.forEach { it() } + addPreTickAction { + spawnAge++ + if (scaleHelper == null || !scalePreTick) { + return@addPreTickAction + } + if (!scaleReversed) { + scaleHelper!!.doScale() + } else { + scaleHelper!!.doScaleReversed() + if (reversedClean && scaleHelper!!.current <= 0) { + clear(false) + } + } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt new file mode 100644 index 00000000..caf3b5e3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.phys.Vec3 + +class SequencedParticleComposition(position: Vec3, world: Level? = null) : ParticleComposition(position, world) { + constructor(world: Level?) : this(Vec3.ZERO, world) + constructor(world: Level?, pos: Vec3) : this(pos, world) + + override fun getCodec(): CommonStreamCodec { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt new file mode 100644 index 00000000..de0e41af --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +class SequencedParticleShapeComposition(uuid: UUID) : ParticleShapeComposition(uuid) { + override fun getCodec(): CommonStreamCodec { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt new file mode 100644 index 00000000..635c52ef --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt @@ -0,0 +1,334 @@ +package cn.coostack.cooparticlesapi.network.particle.composition.manager + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionRotateS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionStateS2C +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import io.netty.buffer.Unpooled +import net.minecraft.network.PacketByteBuf +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.player.Player +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.set +import kotlin.jvm.java + +object ParticleCompositionManager { + private val loadedClientCompositions = WeakIdentitySet() + + val clientView = ConcurrentHashMap() + + val serverView = ConcurrentHashMap() + + val playerPlayerVisibleSet = ConcurrentHashMap>() + + val registeredTypes = ConcurrentHashMap>() + + internal fun setClientLoaded(composition: ParticleComposition, loaded: Boolean) { + if (loaded) { + loadedClientCompositions.add(composition) + } else { + loadedClientCompositions.remove(composition) + } + } + + @JvmStatic + fun loadedClientCount(): Int = loadedClientCompositions.size() + + internal fun debugCompositions(): List = loadedClientCompositions.snapshot() + + fun loadedServerCount(): Int = serverView.size + + fun addClient(composition: ParticleComposition) { + clientView[composition.controlUUID] = composition + composition.display() + } + + fun spawn(composition: ParticleComposition) { + composition.resetLifecycleForSpawn() + removeVisibleComposition(composition) + serverView[composition.controlUUID] = composition + composition.display() + sendCreateOrUpdate(composition) + } + + fun register(randomInstance: ParticleComposition) { + val id = randomInstance::class.java.name + val codec = randomInstance.getCodec() + registeredTypes[id] = codec + } + + fun register(type: Class) { + registeredTypes[type.name] = ParticleCompositionRegistryHelper.generateCodec(type) + } + + fun registerScanner() { + val start = System.currentTimeMillis() + CooParticlesConstants.logger.info("正在自动注册 Compositions") + CooAPIScanner.getWithAnnotation(CooAutoRegister::class.java) + .iterator() + .forEach { + val clazz = it.toClass() + if (!ParticleComposition::class.java.isAssignableFrom(clazz)) { + return@forEach + } + @Suppress("UNCHECKED_CAST") + register(clazz as Class) + } + val end = System.currentTimeMillis() + CooParticlesConstants.logger.info("Compositions 注册完成 耗时 ${end - start} ms") + } + + fun tickClient() { + val iterator = clientView.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (entry.value.canceled) { + iterator.remove() + entry.value.remove() + continue + } + entry.value.tick() + } + } + + fun tickServer() { + val iterator = serverView.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (entry.value.canceled) { + iterator.remove() + sendRemove(entry.value) + continue + } + syncVisible(entry.value, false) + entry.value.tick() + } + } + + fun removeVisibleComposition(composition: ParticleComposition) { + playerPlayerVisibleSet.values.forEach { compositions -> + compositions.remove(composition) + } + } + + fun clearVisibleFor(player: Player) { + playerPlayerVisibleSet.remove(player.uuid) + } + + fun sendCreateOrUpdate(composition: ParticleComposition) { + syncVisible(composition, true) + } + + private fun syncVisible(composition: ParticleComposition, forceUpdate: Boolean) { + val server = CooParticlesAPI.serverOrNull ?: return + val createTargets = ArrayList() + val updateTargets = ArrayList() + val removeTargets = ArrayList() + server.playerList.players.forEach { player -> + val compositions = playerPlayerVisibleSet.getOrPut(player.uuid) { HashSet() } + val shouldView = player.level().dimension() == composition.world?.dimension() && + composition.position.distanceTo(player.position()) <= composition.visibleRange + if (composition in compositions) { + if (shouldView) { + updateTargets.add(player) + } else { + compositions.remove(composition) + removeTargets.add(player) + } + } else if (shouldView) { + createTargets.add(player) + } + } + + if (removeTargets.isNotEmpty()) { + val removePacket = PacketParticleCompositionS2C( + composition.controlUUID, + composition::class.java.name, + ByteArray(0) + ).apply { + distanceRemove = true + } + removeTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(removePacket, it) } + } + + if (createTargets.isEmpty() && updateTargets.isEmpty()) { + return + } + val fullDirty = composition.hasNetworkFullDirty() + val stateDirty = composition.hasNetworkStateDirty() + if (createTargets.isEmpty() && !forceUpdate && !fullDirty) { + if (stateDirty) { + val statePacket = PacketParticleCompositionStateS2C( + composition.controlUUID, + composition.position, + composition.visibleRange, + composition.scale, + composition.status.displayStatus, + composition.status.closedInternal, + composition.status.current, + ) + updateTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(statePacket, it) } + composition.consumeNetworkStateDirty() + } + return + } + val registryAccess = CooParticlesAPI.registryAccessOrNull ?: return + val type = composition::class.java.name + val buf = PacketByteBuf(Unpooled.buffer()) + val data = try { + registeredTypes[type]!!.encode(buf, composition) + ByteArray(buf.readableBytes()).also { buf.readBytes(it) } + } finally { + buf.release() + } + if (createTargets.isNotEmpty()) { + val createPacket = PacketParticleCompositionS2C(composition.controlUUID, type, data).apply { + recreate = true + } + createTargets.forEach { player -> + CooParticlesServices.SERVER_NETWORK.send(createPacket, player) + playerPlayerVisibleSet.getValue(player.uuid).add(composition) + } + } + if (forceUpdate || fullDirty) { + val updatePacket = PacketParticleCompositionS2C(composition.controlUUID, type, data) + updateTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(updatePacket, it) } + } else if (stateDirty) { + val statePacket = PacketParticleCompositionStateS2C( + composition.controlUUID, + composition.position, + composition.visibleRange, + composition.scale, + composition.status.displayStatus, + composition.status.closedInternal, + composition.status.current, + ) + updateTargets.forEach { CooParticlesServices.SERVER_NETWORK.send(statePacket, it) } + } + if (forceUpdate || fullDirty || createTargets.isNotEmpty()) { + composition.consumeNetworkFullDirty() + } + if (stateDirty) { + composition.consumeNetworkStateDirty() + } + } + + fun sendRemove(composition: ParticleComposition) { + val server = CooParticlesAPI.serverOrNull ?: return + val uuid = composition.controlUUID + val type = composition::class.java.name + val packet = PacketParticleCompositionS2C(uuid, type, ByteArray(0)).apply { + distanceRemove = true + } + server.playerList.players.forEach { player -> + val compositions = playerPlayerVisibleSet[player.uuid] ?: return@forEach + if (compositions.remove(composition)) { + CooParticlesServices.SERVER_NETWORK.send(packet, player) + } + } + } + + fun sendRotate(composition: ParticleComposition, direction: RelativeLocation?, rollDelta: Double) { + if (!composition.displayed || composition.canceled) { + return + } + val server = CooParticlesAPI.serverOrNull ?: return + val packet = PacketParticleCompositionRotateS2C( + composition.controlUUID, + direction?.toVector(), + rollDelta + ) + server.playerList.players.forEach { + if (it.level().dimension() != composition.world?.dimension()) { + return@forEach + } + val compositions = playerPlayerVisibleSet[it.uuid] ?: return@forEach + if (composition in compositions) { + CooParticlesServices.SERVER_NETWORK.send(packet, it) + } + } + } + + fun clearClient() { + clientView.values.forEach { + it.clear(true) + } + clientView.clear() + loadedClientCompositions.clear() + } + + fun clearServer() { + serverView.onEach { + it.value.clear(true) + }.clear() + playerPlayerVisibleSet.clear() + } +} + +private class WeakIdentitySet { + private val collectedReferences = ReferenceQueue() + private val references = HashSet>() + + @Synchronized + fun add(value: T) { + removeCollectedReferences() + references.add(IdentityWeakReference(value, collectedReferences)) + } + + @Synchronized + fun remove(value: T) { + removeCollectedReferences() + references.remove(IdentityWeakReference(value)) + } + + @Synchronized + fun size(): Int { + removeCollectedReferences() + return references.size + } + + @Synchronized + fun snapshot(): List { + removeCollectedReferences() + return references.mapNotNull { it.get() } + } + + @Synchronized + fun clear() { + references.clear() + while (collectedReferences.poll() != null) { + } + } + + private fun removeCollectedReferences() { + while (true) { + val reference = collectedReferences.poll() ?: return + references.remove(reference) + } + } +} + +private class IdentityWeakReference( + referent: T, + queue: ReferenceQueue? = null, +) : WeakReference(referent, queue) { + private val identityHashCode = System.identityHashCode(referent) + + override fun hashCode(): Int = identityHashCode + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IdentityWeakReference<*>) return false + return get()?.let { referent -> referent === other.get() } ?: false + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt new file mode 100644 index 00000000..271591c8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.particle.data + +import kotlin.random.Random + +class DoubleRangeData(min: Double, max: Double) : RangeData(min, max) { + fun random(): Double { + if (min - max <= 10e-6) { + return max + } + return Random.nextDouble(min, max) + } +} + +infix fun Double.isIn(range: DoubleRangeData): Boolean { + return this in range.min..range.max +} + +infix fun Double.minRangeTo(max: Double): DoubleRangeData { + return DoubleRangeData(this, max) +} + +infix fun Double.maxRangeTo(min: Double): DoubleRangeData { + return DoubleRangeData(min, this) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt new file mode 100644 index 00000000..09c0d1e3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt @@ -0,0 +1,19 @@ +package cn.coostack.cooparticlesapi.network.particle.data + +import kotlin.random.Random + +class FloatRangeData(min: Float, max: Float) : RangeData(min, max) { + fun random(): Float = Random.nextFloat() * (max - min) + min +} + +infix fun Float.isIn(range: FloatRangeData): Boolean { + return this in range.min..range.max +} + +infix fun Float.minRangeTo(max: Float): FloatRangeData { + return FloatRangeData(this, max) +} + +infix fun Float.maxRangeTo(min: Float): FloatRangeData { + return FloatRangeData(min, this) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt new file mode 100644 index 00000000..3ffeaa4e --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt @@ -0,0 +1,22 @@ +package cn.coostack.cooparticlesapi.network.particle.data + +import kotlin.random.Random + +class IntRangeData(min: Int, max: Int) : RangeData(min, max) { + fun random(): Int { + if (min == max) return min + return Random.nextInt(min, max) + } +} + +infix fun Int.isIn(range: IntRangeData): Boolean { + return this in range.min..range.max +} + +infix fun Int.minRangeTo(max: Int): IntRangeData { + return IntRangeData(this, max) +} + +infix fun Int.maxRangeTo(min: Int): IntRangeData { + return IntRangeData(min, this) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt new file mode 100644 index 00000000..e5285e9c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.data + +import kotlin.random.Random + +abstract class RangeData>(var min: T, var max: T) { + init { + require(min <= max) { "min must be <= $max" } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt new file mode 100644 index 00000000..ec222740 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt @@ -0,0 +1,17 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoEmitters(pos: Vec3, world: Level?) : ClassEmitters(pos, world) { + override fun getEmittersID(): String { + return this::class.java.name + } + + override fun getCodec(): ForgeStreamCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt new file mode 100644 index 00000000..1db5725b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt @@ -0,0 +1,17 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoParticleEmitters(pos: Vec3, world: Level?) : ClassParticleEmitters(pos, world) { + override fun getEmittersID(): String { + return this::class.java.name + } + + override fun getCodec(): ForgeStreamCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt new file mode 100644 index 00000000..fd7eba4d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt @@ -0,0 +1,16 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoTransformableCParticleEmitter(pos: Vec3, world: Level?) : + TransformableCParticleEmitter(pos, world) { + final override fun getEmittersID(): String = this::class.java.name + + final override fun getCodec(): ForgeStreamCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt new file mode 100644 index 00000000..1309263f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt @@ -0,0 +1,342 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.SerializableData +import cn.coostack.cooparticlesapi.api.controler.Tickable +import cn.coostack.cooparticlesapi.display.DisplayEntity +import cn.coostack.cooparticlesapi.extend.lengthCoerceAtMost +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.GlobalWindDirection +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.WindDirection +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.WindDirections +import cn.coostack.cooparticlesapi.network.particle.emitters.event.ParticleEventHandler +import cn.coostack.cooparticlesapi.network.particle.emitters.event.ParticleEventHandlerManager +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.PhysicsUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.interpolator.Interpolator +import cn.coostack.cooparticlesapi.utils.interpolator.emitters.LineEmitterInterpolator +import net.minecraft.client.Minecraft +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.core.BlockPos +import net.minecraft.core.Direction +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.Vec3 +import java.util.ArrayList +import java.util.HashMap +import java.util.SortedMap +import java.util.TreeMap +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max + +abstract class ClassEmitters( + pos: Vec3, + override var world: Level?, +) : ParticleEmitters { + private val posState = dirty(pos) + override var pos by posState + override var tick: Int = 0 + override var maxTick: Int = 120 + override var delay: Int = 0 + override var uuid: UUID = UUID.randomUUID() + override var canceled: Boolean = false + override var playing: Boolean = false + var airDensity = 0.0 + var gravity: Double = 0.0 + val handlerList = ConcurrentHashMap>() + + var enableInterpolator = false + + var emittersInterpolator: Interpolator = LineEmitterInterpolator() + .setRefiner(5.0) + + override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + val handlerID = handler.getHandlerID() + if (!ParticleEventHandlerManager.hasRegister(handlerID)) { + ParticleEventHandlerManager.register(handler) + } + val eventID = handler.getTargetEventID() + val list = handlerList.getOrPut(eventID) { TreeMap() } + list[handler] = innerClass + } + + private fun addEventHandlerList(list: MutableList) { + val dirtyLists = HashMap>() + list.forEach { handler -> + val handlerID = handler.getHandlerID() + if (!ParticleEventHandlerManager.hasRegister(handlerID)) { + ParticleEventHandlerManager.register(handler) + } + val eventID = handler.getTargetEventID() + val handlers = handlerList.getOrPut(eventID) { TreeMap() } + handlers[handler] = false + } + dirtyLists.forEach { + it.value.sortBy { h -> h.getPriority() } + } + } + + private fun collectEventHandles(): List { + return handlerList.flatMap { + it.value.filter { item -> !item.value }.keys + } + } + + companion object { + fun encodeBase(data: ClassEmitters, buf: PacketByteBuf) { + val handles = data.collectEventHandles() + buf.writeInt(handles.size) + handles.forEach { + buf.writeUtf(it.getHandlerID()) + } + buf.writeVec3(data.pos) + buf.writeInt(data.tick) + buf.writeInt(data.maxTick) + buf.writeInt(data.delay) + buf.writeUUID(data.uuid) + buf.writeBoolean(data.canceled) + buf.writeBoolean(data.playing) + buf.writeDouble(data.gravity) + buf.writeDouble(data.airDensity) + buf.writeDouble(data.mass) + buf.writeBoolean(data.enableInterpolator) + buf.writeDouble(data.emittersInterpolator.refinerCount) + buf.writeUtf(data.wind.getID()) + data.wind.getCodec().encode(buf, data.wind) + } + + fun decodeBase(container: ClassEmitters, buf: PacketByteBuf) { + val handlerCount = buf.readInt() + val handlers = ArrayList() + repeat(handlerCount) { + val handleID = buf.readUtf() + val handler = ParticleEventHandlerManager.getHandlerById(handleID)!! + handlers.add(handler) + } + container.addEventHandlerList(handlers) + + val pos = buf.readVec3() + val tick = buf.readInt() + val maxTick = buf.readInt() + val delay = buf.readInt() + val uuid = buf.readUUID() + val canceled = buf.readBoolean() + val playing = buf.readBoolean() + val gravity = buf.readDouble() + val airDensity = buf.readDouble() + val mass = buf.readDouble() + val enableInterpolator = buf.readBoolean() + val interpolatorCount = buf.readDouble() + val windID = buf.readUtf() + val wind = WindDirections.getCodecFromID(windID).decode(buf) + container.apply { + this.posState.setCodecValue(pos) + this.tick = tick + this.maxTick = maxTick + this.delay = delay + this.uuid = uuid + this.canceled = canceled + this.playing = playing + this.gravity = gravity + this.airDensity = airDensity + this.mass = mass + this.wind = wind + this.enableInterpolator = enableInterpolator + this.emittersInterpolator.setRefiner(interpolatorCount) + } + } + } + + var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { + it.loadEmitters(this) + } + + var mass: Double = 1.0 + + override fun start() { + if (playing) return + playing = true + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + override fun stop() { + canceled = true + } + + override fun tick() { + if (canceled || !playing) return + world ?: return + doTick() + if (!world!!.isClientSide) { + increaseTick() + return + } + + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + + if (tick % max(1, delay) == 0) { + if (enableInterpolator) { + val res = emittersInterpolator.getRefinedResult() + val count = res.size + res.forEachIndexed { index, relative -> + val current = relative.toVector() + val lerpProgress = index / (count - 1f) + doSubtick(current, lerpProgress) + spawnParticle(current, lerpProgress) + } + } else { + spawnParticle(pos, 1f) + } + } + increaseTick() + } + + private fun increaseTick() { + if (++tick >= maxTick && maxTick != -1) { + stop() + } + } + + override fun spawnParticle(pos: Vec3, lerpProgress: Float) { + if (!world!!.isClientSide) return + val spawnWorld = world as ClientLevel + val controls = genControls(lerpProgress) + val total = controls.size.coerceAtLeast(1).toFloat() + var spawnedCount = 0f + controls.forEach { (data, relative) -> + spawnedCount++ + val spawnPos = pos.add(relative.toVector()) + val particleLerpProgress = spawnedCount / total + if (!isVisibleToClient(data, spawnPos)) { + return@forEach + } + val control = data.createControler( + spawnWorld, + spawnPos, + particleLerpProgress, + lerpProgress + ) + val displayed = data.getDisplayer().display(spawnPos, spawnWorld) ?: control + singleControlableAction( + displayed, + data, + RelativeLocation.of(spawnPos), + spawnWorld, + particleLerpProgress, + lerpProgress + ) + bindControlerMotion(displayed, data, spawnWorld) + } + } + + protected open fun resolveVisibleRange(data: SerializableData): Float { + return when (data) { + is ControlableParticleData -> data.visibleRange + is DisplayEntityEmittersData -> data.visibleRange + else -> -1f + } + } + + private fun isVisibleToClient(data: SerializableData, spawnPos: Vec3): Boolean { + val visibleRange = resolveVisibleRange(data) + if (visibleRange < 0f) { + return true + } + val player = Minecraft.getInstance().player ?: return false + return player.position().distanceTo(spawnPos) <= visibleRange + } + + abstract fun doTick() + + abstract fun genControls(lerpProgress: Float): List> + + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + abstract fun singleControlableAction( + controler: Controlable<*>, + data: SerializableData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + protected open fun moveSingleControler( + controler: Controlable<*>, + data: SerializableData, + to: Vec3, + collide: BlockHitResult + ) { + controler.teleportTo(to) + } + + protected open fun resolveControlerPos( + controler: Controlable<*>, + data: SerializableData + ): Vec3? { + val value = runCatching { controler.getControlObject() }.getOrNull() ?: return null + return when (value) { + is ControlableParticle -> value.loc + is ParticleComposition -> value.position + is DisplayEntity -> value.pos + else -> null + } + } + + protected open fun resolveControlerVelocity( + controler: Controlable<*>, + data: SerializableData + ): Vec3 { + if (data is ControlableParticleData) { + data.velocity = data.velocity.lengthCoerceAtMost(data.speedLimit) + return data.velocity + } + return Vec3.ZERO + } + + private fun bindControlerMotion( + controler: Controlable<*>, + data: SerializableData, + spawnWorld: ClientLevel + ) { + val tickable = controler as? Tickable<*> ?: return + @Suppress("UNCHECKED_CAST") + (tickable as Tickable).addPreTickAction { + val current = resolveControlerPos(controler, data) ?: return@addPreTickAction + val velocity = resolveControlerVelocity(controler, data) + if (velocity.lengthSqr() <= 0.001) { + return@addPreTickAction + } + val to = current.add(velocity) + val collide = if (velocity.length() <= 200) { + PhysicsUtil.collide(current, velocity, spawnWorld) + } else { + BlockHitResult.miss(current, Direction.UP, BlockPos.containing(current)) + } + moveSingleControler(controler, data, to, collide) + } + } + + override fun update(emitters: ParticleEmitters) { + if (emitters !is ClassEmitters) return + this.posState.setCodecValue(emitters.pos) + this.world = emitters.world + this.tick = emitters.tick + this.maxTick = emitters.maxTick + this.delay = emitters.delay + this.uuid = emitters.uuid + this.canceled = emitters.canceled + this.playing = emitters.playing + this.handlerList.putAll(emitters.handlerList) + this.emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) + ParticleEmittersRegistryHelper.updateEmitter(this, emitters) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt new file mode 100644 index 00000000..bff348d4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt @@ -0,0 +1,471 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import cn.coostack.cooparticlesapi.cparticle.CParticleSystemManager +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleEmitterBridge +import cn.coostack.cooparticlesapi.cparticle.force.CParticleForce +import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceSink +import cn.coostack.cooparticlesapi.extend.asVec3 +import cn.coostack.cooparticlesapi.extend.lengthCoerceAtMost +import cn.coostack.cooparticlesapi.extend.ofFloored +import cn.coostack.cooparticlesapi.extend.times +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.GlobalWindDirection +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.WindDirection +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.WindDirections +import cn.coostack.cooparticlesapi.network.particle.emitters.event.* +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.particles.control.ParticleControler +import cn.coostack.cooparticlesapi.particles.control.RemoveReason +import cn.coostack.cooparticlesapi.utils.PhysicsUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.interpolator.Interpolator +import cn.coostack.cooparticlesapi.utils.interpolator.emitters.LineEmitterInterpolator +import net.minecraft.client.Minecraft +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.core.BlockPos +import net.minecraft.core.Direction +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.entity.Entity +import net.minecraft.world.level.Level +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.world.phys.Vec3 +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max +import kotlin.math.pow + +abstract class ClassParticleEmitters( + pos: Vec3, + override var world: Level?, +) : ParticleEmitters { + private val posState = dirty(pos) + override var pos by posState + override var tick: Int = 0 + override var maxTick: Int = 120 + override var delay: Int = 0 + override var uuid: UUID = UUID.randomUUID() + override var canceled: Boolean = false + override var playing: Boolean = false + var airDensity = 0.0 + var gravity: Double = 0.0 + private var lastTickPos: Vec3 = pos + var emitterVelocity: Vec3 = Vec3.ZERO + private set + val handlerList = ConcurrentHashMap>() + + var enableInterpolator = false + + var emittersInterpolator: Interpolator = LineEmitterInterpolator() + .setRefiner(5.0) + + override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + val handlerID = handler.getHandlerID() + if (!ParticleEventHandlerManager.hasRegister(handlerID)) { + ParticleEventHandlerManager.register(handler) + } + val eventID = handler.getTargetEventID() + val handlerList = handlerList.getOrPut(eventID) { TreeMap() } + handlerList[handler] = innerClass + } + + private fun addEventHandlerList(list: MutableList) { + val dirtyLists = HashMap>() + list.forEach { handler -> + val handlerID = handler.getHandlerID() + if (!ParticleEventHandlerManager.hasRegister(handlerID)) { + ParticleEventHandlerManager.register(handler) + } + val eventID = handler.getTargetEventID() + val handlerList = handlerList.getOrPut(eventID) { TreeMap() } + handlerList[handler] = false + } + dirtyLists.forEach { + it.value.sortBy { it -> it.getPriority() } + } + } + + private fun collectEventHandles(): List { + return handlerList.flatMap { + it.value.filter { it -> + !it.value + }.keys + } + } + + companion object { + fun encodeBase(data: ClassParticleEmitters, buf: PacketByteBuf) { + val handles = data.collectEventHandles() + buf.writeInt(handles.size) + handles.forEach { + val id = it.getHandlerID() + buf.writeUtf(id) + } + buf.writeVec3(data.pos) + buf.writeInt(data.tick) + buf.writeInt(data.maxTick) + buf.writeInt(data.delay) + buf.writeUUID(data.uuid) + buf.writeBoolean(data.canceled) + buf.writeBoolean(data.playing) + buf.writeDouble(data.gravity) + buf.writeDouble(data.airDensity) + buf.writeDouble(data.mass) + buf.writeBoolean(data.enableInterpolator) + buf.writeDouble(data.emittersInterpolator.refinerCount) + buf.writeUtf(data.wind.getID()) + data.wind.getCodec().encode(buf, data.wind) + } + + fun decodeBase(container: ClassParticleEmitters, buf: PacketByteBuf) { + val handlerCount = buf.readInt() + val handlerList = ArrayList() + repeat(handlerCount) { + val handleID = buf.readUtf() + val handler = ParticleEventHandlerManager.getHandlerById(handleID)!! + handlerList.add(handler) + } + container.addEventHandlerList(handlerList) + val pos = buf.readVec3() + val tick = buf.readInt() + val maxTick = buf.readInt() + val delay = buf.readInt() + val uuid = buf.readUUID() + val canceled = buf.readBoolean() + val playing = buf.readBoolean() + val gravity = buf.readDouble() + val airDensity = buf.readDouble() + val mass = buf.readDouble() + val enableInterpolator = buf.readBoolean() + val interpolatorCount = buf.readDouble() + val id = buf.readUtf() + val wind = WindDirections.getCodecFromID(id) + .decode(buf) + container.apply { + this.posState.setCodecValue(pos) + this.tick = tick + this.maxTick = maxTick + this.delay = delay + this.uuid = uuid + this.canceled = canceled + this.airDensity = airDensity + this.gravity = gravity + this.mass = mass + this.playing = playing + this.airDensity = airDensity + this.wind = wind + this.enableInterpolator = enableInterpolator + this.emittersInterpolator.setRefiner(interpolatorCount) + } + } + } + + var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { + it.loadEmitters(this) + } + + open fun cparticleForces(): List = emptyList() + + open fun submitCParticleForces(sink: CParticleForceSink) { + sink.submitAll(cparticleForces()) + } + + open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE + + var mass: Double = 1.0 + override fun start() { + if (playing) return + playing = true + lastTickPos = pos + emitterVelocity = Vec3.ZERO + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + override fun stop() { + canceled = true + } + + override fun tick() { + if (canceled || !playing) { + return + } + + world ?: return + val previousPos = lastTickPos + doTick() + emitterVelocity = pos.subtract(previousPos) + lastTickPos = pos + if (!world!!.isClientSide) { + increaseTick() + return + } + CParticleEmitterBridge.syncSystems(this) + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + if (tick % max(1, delay) == 0) { + if (enableInterpolator) { + val res = emittersInterpolator.getRefinedResult() + val count = res.size + res.forEachIndexed { index, it -> + val pos = it.toVector() + val lerpProgress = index / (count - 1F) + doSubtick(pos, lerpProgress) + spawnParticle(pos, lerpProgress) + } + } else { + spawnParticle(pos, 1F) + } + } + increaseTick() + } + + private fun increaseTick() { + if (++tick >= maxTick && maxTick != -1) { + stop() + } + } + + override fun spawnParticle(pos: Vec3, lerpProgress: Float) { + if (!world!!.isClientSide) { + return + } + val world = world as ClientLevel + var spawnedCount = 0F + val particles = genParticles(lerpProgress) + val total = particles.size + val cparticleBatchSize = particles.count { it.first is ControlableCParticleData } + particles.forEach { + spawnedCount++ + spawnParticle( + world, + pos.add(it.second.toVector()), + it.first, + spawnedCount / total, + lerpProgress, + cparticleBatchSize, + ) + } + } + + abstract fun doTick() + + abstract fun genParticles(lerpProgress: Float): List> + + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + abstract fun singleParticleAction( + controler: ParticleControler, + data: ControlableParticleData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + open fun singleParticleDeathAction( + oldControler: ParticleControler, + oldData: ControlableParticleData, + respawnCount: Int, + reason: RemoveReason + ): List> { + return listOf() + } + + private fun spawnParticle( + world: ClientLevel, + pos: Vec3, + data: ControlableParticleData, + particleLerpProgress: Float, + posLerpProgress: Float, + cparticleBatchSize: Int, + ) { + val player = Minecraft.getInstance().player ?: return + if (player.position().distanceTo(pos) > data.visibleRange) { + return + } + if (data is ControlableCParticleData && + CParticleEmitterBridge.trySpawn(this, world, pos, data, cparticleBatchSize) + ) { + return + } + val effect = data.effect + effect.controlUUID = data.uuid + val displayer = data.getDisplayer() + val control = data.createControler(world, pos, particleLerpProgress, posLerpProgress) as ParticleControler + control.addPreTickAction { + val hitEntityHandlers = handlerList[ParticleHitEntityEvent.EVENT_ID] ?: return@addPreTickAction + if (hitEntityHandlers.isEmpty()) return@addPreTickAction + val entities = + world.getEntitiesOfClass(Entity::class.java, this.bounding.expandTowards(0.5, 0.5, 0.5)) { true } + if (entities.isEmpty()) return@addPreTickAction + val first = entities.first() + val event = ParticleHitEntityEvent(this, data, first) + for ((handler, _) in hitEntityHandlers) { + if (handler.getTargetEventID() != ParticleHitEntityEvent.EVENT_ID) { + continue + } + handler.handle(event) + if (event.canceled) { + break + } + } + } + + control.addPreTickAction { + val hitEntityHandlers = handlerList[ParticleOnLiquidEvent.EVENT_ID] ?: return@addPreTickAction + if (hitEntityHandlers.isEmpty()) return@addPreTickAction + val blockPos = ofFloored(this.loc) + val beforeLiquid = (control.bufferedData["cross_liquid"] as? Boolean) ?: false + val state = world.getBlockState(blockPos) + val currentLiquid = !state.isSolid + control.bufferedData["cross_liquid"] = currentLiquid + if (beforeLiquid || !currentLiquid) { + return@addPreTickAction + } + val event = ParticleOnLiquidEvent(this, data, blockPos) + for ((handler, _) in hitEntityHandlers) { + if (handler.getTargetEventID() != ParticleOnLiquidEvent.EVENT_ID) { + continue + } + handler.handle(event) + if (event.canceled) { + break + } + } + } + val p = RelativeLocation.of(pos) + singleParticleAction(control, data, p, world, particleLerpProgress, posLerpProgress) + control.applyDestroyAction { + val newParticles = + singleParticleDeathAction(control, data, data.respawnCount + 1, it) + val respawnCParticleBatchSize = newParticles.count { (newData, _) -> + newData is ControlableCParticleData + } + newParticles.forEach { (newData, rel) -> + newData.respawnCount = data.respawnCount + 1 + spawnParticle( + world, + this.loc.add(rel.toVector()), + newData, + particleLerpProgress, + posLerpProgress, + respawnCParticleBatchSize, + ) + } + } + control.addPreTickAction { + if (currentAge++ >= lifetime) { + remove() + } + if (minecraftTick) return@addPreTickAction + if (bounding.hasNaN()) return@addPreTickAction + + data.velocity = data.velocity.lengthCoerceAtMost(data.speedLimit) + val prepareMove = this.loc.add(data.velocity) + val clipRes = if (data.velocity.lengthSqr() > 0.001) { + if (data.velocity.length() <= 200) { + PhysicsUtil.collide(this.loc, data.velocity, world) + } else { + BlockHitResult.miss(this.loc, Direction.UP, BlockPos.containing(this.loc)) + } + } else { + BlockHitResult.miss(this.loc, Direction.UP, BlockPos.containing(this.loc)) + } + onTheGround = clipRes.type != HitResult.Type.MISS && clipRes.direction == Direction.UP + moveSingleParticleWithVelocity(this, data, prepareMove, clipRes) + if (onTheGround) { + val offset = clipRes.direction.normal.asVec3() * 0.1 + val event = ParticleOnGroundEvent( + this, + data, + ofFloored(prepareMove), + clipRes.location.add(offset), + clipRes + ) + for ((handler, _) in (handlerList[ParticleOnGroundEvent.EVENT_ID] ?: emptyMap())) { + if (handler.getTargetEventID() != ParticleOnGroundEvent.EVENT_ID) { + continue + } + handler.handle(event) + if (event.canceled) { + break + } + } + } + + if (clipRes.type != HitResult.Type.MISS) { + val event = ParticleCollideEvent( + this, data, clipRes + ) + for ((handler, _) in (handlerList[ParticleCollideEvent.EVENT_ID] ?: emptyMap())) { + if (handler.getTargetEventID() != ParticleCollideEvent.EVENT_ID) { + continue + } + handler.handle(event) + if (event.canceled) { + break + } + } + } + } + if (displayer.display(p.toVector(), world) == null) { + control.remove(RemoveReason.QUEUE) + } + } + + fun updatePhysics(pos: Vec3, data: ControlableParticleData, particle: ControlableParticle) { + val v = data.velocity + val speed = v.length() + val gravity = if (particle.onTheGround) 0.0 else gravity + val gravityForce = Vec3(0.0, -gravity, 0.0) + val airResistanceForce = if (speed > 0.01) { + val dragMagnitude = 0.5 * airDensity * PhysicConstant.DRAG_COEFFICIENT * + PhysicConstant.CROSS_SECTIONAL_AREA * speed.pow(2) * 0.05 + v.normalize().scale(-dragMagnitude) + } else { + Vec3.ZERO + } + + if (!wind.hasLoadedEmitters()) { + wind.loadEmitters(this) + } + + val windForce = WindDirections.handleWindForce( + wind, pos, + airDensity, PhysicConstant.DRAG_COEFFICIENT, PhysicConstant.CROSS_SECTIONAL_AREA, v + ) + + val a = gravityForce + .add(airResistanceForce) + .add(windForce) + + data.velocity = v.add(a) + } + + protected open fun moveSingleParticleWithVelocity( + particle: ControlableParticle, + data: ControlableParticleData, + to: Vec3, + collide: BlockHitResult + ) { + particle.teleportTo(to) + } + + override fun update(emitters: ParticleEmitters) { + if (emitters !is ClassParticleEmitters) return + this.posState.setCodecValue(emitters.pos) + this.world = emitters.world + this.tick = emitters.tick + this.maxTick = emitters.maxTick + this.delay = emitters.delay + this.uuid = emitters.uuid + this.canceled = emitters.canceled + this.playing = emitters.playing + this.handlerList.putAll(emitters.handlerList) + this.emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) + ParticleEmittersRegistryHelper.updateEmitter(this, emitters) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt new file mode 100644 index 00000000..8a53bd0c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt @@ -0,0 +1,54 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import net.minecraft.network.PacketByteBuf + +open class CompositionEmittersData : ControlableParticleData() { + companion object { + val CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, data -> + ControlableParticleData.PACKET_CODEC.encode(buf, data) + }, + { buf -> + val data = ControlableParticleData.PACKET_CODEC.decode(buf) as ControlableParticleData + CompositionEmittersData().also { + it.uuid = data.uuid + it.velocity = data.velocity + it.weightSize = data.weightSize + it.heightSize = data.heightSize + it.uniformSize = data.uniformSize + it.visibleRange = data.visibleRange + it.color = Vector3f(data.color) + it.alpha = data.alpha + it.age = data.age + it.maxAge = data.maxAge + it.textureSheet = data.textureSheet + it.effect = data.effect + it.speed = data.speed + it.speedLimit = data.speedLimit + it.sign = data.sign + it.light = data.light + it.cameraOption = data.cameraOption + it.axis = data.axis + it.yaw = data.yaw + it.pitch = data.pitch + it.roll = data.roll + it.depthSize = data.depthSize + } + } + ) + } + + var composition: ParticleComposition? = null + + override fun getCodec(): ForgeStreamCodec { + return CODEC + } + + override fun clone(): SerializableData { + return super.clone().also { + val data = it as CompositionEmittersData + data.composition = composition + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt new file mode 100644 index 00000000..d223548f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt @@ -0,0 +1,58 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.cparticle.CParticleColorCurve +import cn.coostack.cooparticlesapi.cparticle.CParticleCurve +import cn.coostack.cooparticlesapi.cparticle.CParticleTextureSource +import cn.coostack.cooparticlesapi.cparticle.CParticleUpdateMode +import org.joml.Vector3f + +open class ControlableCParticleData : ControlableParticleData() { + + var textureSource: CParticleTextureSource? = null + var colorCurve: CParticleColorCurve = CParticleColorCurve.linear(Vector3f(1f, 1f, 1f), Vector3f(1f, 1f, 1f)) + var alphaCurve: CParticleCurve = CParticleCurve.constant(1.0) + var updateMode: CParticleUpdateMode = CParticleUpdateMode.ALWAYS + var rotation: Vector3f = Vector3f(0f, 0f, 0f) + + companion object { + val CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, data -> + ControlableParticleData.PACKET_CODEC.encode(buf, data) + buf.writeBoolean(data.textureSource != null) + if (data.textureSource != null) { + CParticleTextureSource.STREAM_CODEC.encode(buf, data.textureSource!!) + } + CParticleColorCurve.STREAM_CODEC.encode(buf, data.colorCurve) + CParticleCurve.STREAM_CODEC.encode(buf, data.alphaCurve) + buf.writeEnum(data.updateMode) + buf.writeVector3f(data.rotation) + }, + { buf -> + val data = ControlableParticleData.PACKET_CODEC.decode(buf) as ControlableCParticleData + if (buf.readBoolean()) { + data.textureSource = CParticleTextureSource.STREAM_CODEC.decode(buf) + } + data.colorCurve = CParticleColorCurve.STREAM_CODEC.decode(buf) + data.alphaCurve = CParticleCurve.STREAM_CODEC.decode(buf) + data.updateMode = buf.readEnum() + data.rotation = buf.readVector3f() + data + } + ) + } + + override fun getCodec(): ForgeStreamCodec { + return CODEC + } + + override fun clone(): SerializableData { + return super.clone().also { + val data = it as ControlableCParticleData + data.textureSource = textureSource + data.colorCurve = colorCurve + data.alphaCurve = alphaCurve + data.updateMode = updateMode + data.rotation = Vector3f(rotation) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt new file mode 100644 index 00000000..302701c6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt @@ -0,0 +1,157 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.codec.ForgeCodecHelper +import cn.coostack.cooparticlesapi.api.controler.SerializableData +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.particles.ParticleCameraOption +import cn.coostack.cooparticlesapi.particles.control.ControlParticleManager +import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect +import cn.coostack.cooparticlesapi.supports.TextureSheetsEnum +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.client.particle.ParticleRenderType +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.phys.Vec3 +import org.joml.Vector3f +import java.util.UUID + +open class ControlableParticleData : SerializableData { + companion object { + @JvmStatic + val particleTexturesMapper: MutableMap = mutableMapOf() + + @JvmStatic + val PACKET_CODEC: ForgeStreamCodec = + ForgeStreamCodec.of( + ::encodeBase, + { buf -> decodeBase(buf, ControlableParticleData()) }, + ) + + internal fun encodeBase(buf: PacketByteBuf, data: ControlableParticleData) { + buf.writeUUID(data.uuid) + buf.writeVec3(data.velocity) + buf.writeFloat(data.weightSize) + buf.writeFloat(data.heightSize) + buf.writeBoolean(data.uniformSize) + buf.writeFloat(data.visibleRange) + buf.writeVector3f(data.color) + buf.writeFloat(data.alpha) + buf.writeInt(data.age) + buf.writeInt(data.maxAge) + buf.writeUtf(data.textureSheet) + + ForgeCodecHelper.particleCodecOf(data.effect).encode(buf, data.effect) + + buf.writeDouble(data.speed) + buf.writeDouble(data.speedLimit) + buf.writeInt(data.sign) + buf.writeInt(data.light) + ParticleCameraOption.STREAM_CODEC.encode(buf, data.cameraOption) + buf.writeVec3(data.axis) + buf.writeFloat(data.yaw) + buf.writeFloat(data.pitch) + buf.writeFloat(data.roll) + buf.writeFloat(data.depthSize) + } + + internal fun decodeBase(buf: PacketByteBuf, data: ControlableParticleData): ControlableParticleData { + data.uuid = buf.readUUID() + data.velocity = buf.readVec3() + data.weightSize = buf.readFloat() + data.heightSize = buf.readFloat() + data.uniformSize = buf.readBoolean() + data.visibleRange = buf.readFloat() + data.color = buf.readVector3f() + data.alpha = buf.readFloat() + data.age = buf.readInt() + data.maxAge = buf.readInt() + data.textureSheet = buf.readUtf() + + data.effect = ForgeCodecHelper.particleCodecOf(data.effect).decode(buf) as ControlableParticleEffect + + data.speed = buf.readDouble() + data.speedLimit = buf.readDouble() + data.sign = buf.readInt() + data.light = buf.readInt() + data.cameraOption = ParticleCameraOption.STREAM_CODEC.decode(buf) + data.axis = buf.readVec3() + data.yaw = buf.readFloat() + data.pitch = buf.readFloat() + data.roll = buf.readFloat() + data.depthSize = buf.readFloat() + return data + } + } + + var uuid: UUID = UUID.randomUUID() + var velocity: Vec3 = Vec3.ZERO + var weightSize: Float = 0.3f + var heightSize: Float = 0.3f + var uniformSize: Boolean = true + var visibleRange: Float = 256f + var color: Vector3f = Vector3f(1f, 1f, 1f) + var alpha: Float = 1f + var age: Int = 0 + var maxAge: Int = 20 + var textureSheet: String = "" + var effect: ControlableParticleEffect = ControlableEndRodEffect.codec + var speed: Double = 0.0 + var speedLimit: Double = 1.0 + var sign: Int = 0 + var light: Int = 15 + var cameraOption: ParticleCameraOption = ParticleCameraOption.BILLBOARD + var axis: Vec3 = Vec3.ZERO + var yaw: Float = 0f + var pitch: Float = 0f + var roll: Float = 0f + var depthSize: Float = 0.3f + + override fun getCodec(): ForgeStreamCodec { + return PACKET_CODEC + } + + override fun clone(): SerializableData { + return ControlableParticleData().also { + it.uuid = uuid + it.velocity = velocity + it.weightSize = weightSize + it.heightSize = heightSize + it.uniformSize = uniformSize + it.visibleRange = visibleRange + it.color = Vector3f(color) + it.alpha = alpha + it.age = age + it.maxAge = maxAge + it.textureSheet = textureSheet + it.effect = effect.clone() + it.speed = speed + it.speedLimit = speedLimit + it.sign = sign + it.light = light + it.cameraOption = cameraOption + it.axis = axis + it.yaw = yaw + it.pitch = pitch + it.roll = roll + it.depthSize = depthSize + } + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + return ControlParticleManager.createControler(world, pos, this, particleLerpProcess, posLerpProcess) + } + + override fun getDisplayer(): ParticleDisplayer { + return ParticleDisplayer() + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt new file mode 100644 index 00000000..a2029366 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt @@ -0,0 +1,54 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.display.DisplayEntity +import net.minecraft.network.PacketByteBuf + +open class DisplayEntityEmittersData : ControlableParticleData() { + companion object { + val CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, data -> + ControlableParticleData.PACKET_CODEC.encode(buf, data) + }, + { buf -> + val data = ControlableParticleData.PACKET_CODEC.decode(buf) as ControlableParticleData + DisplayEntityEmittersData().also { + it.uuid = data.uuid + it.velocity = data.velocity + it.weightSize = data.weightSize + it.heightSize = data.heightSize + it.uniformSize = data.uniformSize + it.visibleRange = data.visibleRange + it.color = Vector3f(data.color) + it.alpha = data.alpha + it.age = data.age + it.maxAge = data.maxAge + it.textureSheet = data.textureSheet + it.effect = data.effect + it.speed = data.speed + it.speedLimit = data.speedLimit + it.sign = data.sign + it.light = data.light + it.cameraOption = data.cameraOption + it.axis = data.axis + it.yaw = data.yaw + it.pitch = data.pitch + it.roll = data.roll + it.depthSize = data.depthSize + } + } + ) + } + + var displayEntity: DisplayEntity? = null + + override fun getCodec(): ForgeStreamCodec { + return CODEC + } + + override fun clone(): SerializableData { + return super.clone().also { + val data = it as DisplayEntityEmittersData + data.displayEntity = displayEntity + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt new file mode 100644 index 00000000..cc856462 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt @@ -0,0 +1,81 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.api.NetworkDirtyMarkable +import cn.coostack.cooparticlesapi.api.controler.server.ServerControler +import cn.coostack.cooparticlesapi.network.particle.emitters.event.ParticleEventHandler +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.server.level.ServerLevel +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +interface ParticleEmitters : ServerControler, NetworkDirtyMarkable { + var pos: Vec3 + var world: Level? + var tick: Int + + var maxTick: Int + var delay: Int + var uuid: UUID + var canceled: Boolean + var playing: Boolean + + fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) + + fun getEmittersID(): String + + fun start() + + fun stop() + + fun tick() + + fun spawnParticle(pos: Vec3, lerpProgress: Float) + + fun update(emitters: ParticleEmitters) + + fun getCodec(): ForgeStreamCodec + + override fun getValue(): ParticleEmitters { + return this + } + + override fun markDirty() { + if (world?.isClientSide != true) { + ParticleEmittersManager.enqueueDirty(this) + } + } + + override fun remove() { + canceled = true + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ServerLevel) return + this.world = world + this.pos = pos + ParticleEmittersManager.spawnEmitters(this) + } + + override fun isValid(): Boolean { + return !canceled + } + + override fun rotateAsAxis(radian: Double) { + } + + override fun rotateToPoint(to: RelativeLocation) { + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + } + + override fun teleportTo(to: Vec3) { + if (pos == to) return + pos = to + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt new file mode 100644 index 00000000..0ef07350 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt @@ -0,0 +1,372 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleEmitterBridge +import cn.coostack.cooparticlesapi.event.CooEventBus +import cn.coostack.cooparticlesapi.event.events.particle.emitter.EmitterRemoveEvent +import cn.coostack.cooparticlesapi.event.events.particle.emitter.EmitterSpawnEvent +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleEmittersS2C +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.player.Player +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.HashSet +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +object ParticleEmittersManager { + val emittersCodec = HashMap>() + + val serverEmitters = HashMap() + internal val visible = ConcurrentHashMap>() + private val dirtyEmitters = ConcurrentHashMap.newKeySet() + + val clientEmitters = ConcurrentHashMap() + + fun clientEmitterCount(): Int = clientEmitters.size + + fun serverEmitterCount(): Int = serverEmitters.size + + fun getCodecFromID(id: String): ForgeStreamCodec? { + return emittersCodec[id] + } + + @JvmStatic + fun register( + id: String, + codec: ForgeStreamCodec + ): ForgeStreamCodec { + emittersCodec[id] = codec + return codec + } + + @JvmStatic + fun register(randomInstance: ParticleEmitters) { + val codec = randomInstance.getCodec() + val id = randomInstance.getEmittersID() + register(id, codec) + } + + @JvmStatic + fun addEmitters(emitters: ParticleEmitters) { + if (emitters.world == null) return + if (!emitters.world!!.isClientSide) return + clientEmitters[emitters.uuid] = emitters + emitters.start() + CooEventBus.call(EmitterSpawnEvent(emitters, true)) + } + + @JvmStatic + fun spawnEmitters(emitters: ParticleEmitters) { + if (emitters.world == null) return + if (emitters.world!!.isClientSide) return + serverEmitters[emitters.uuid] = emitters + emitters.start() + updateClientVisible(emitters) + dirtyEmitters.remove(emitters.uuid) + } + + fun createClient(emitters: ParticleEmitters, viewWorld: Level) { + emitters.world = viewWorld + clientEmitters.remove(emitters.uuid)?.let { previous -> + previous.canceled = true + finishClientSystems(previous) + CooEventBus.call(EmitterRemoveEvent(previous, true)) + } + emitters.canceled = false + emitters.playing = false + clientEmitters[emitters.uuid] = emitters + emitters.start() + CooEventBus.call(EmitterSpawnEvent(emitters, true)) + } + + fun changeClient(emitters: ParticleEmitters, viewWorld: Level) { + val current = clientEmitters[emitters.uuid] ?: return + if (current.canceled) return + emitters.world = viewWorld + current.update(emitters) + current.world = viewWorld + } + + fun removeClient(uuid: UUID) { + val emitters = clientEmitters.remove(uuid) ?: return + emitters.canceled = true + finishClientSystems(emitters) + CooEventBus.call(EmitterRemoveEvent(emitters, true)) + } + + fun createOrChangeClient(emitters: ParticleEmitters, viewWorld: Level) { + if (clientEmitters.containsKey(emitters.uuid)) { + changeClient(emitters, viewWorld) + } else { + createClient(emitters, viewWorld) + } + } + + fun doTickServer() { + val iterator = serverEmitters.iterator() + while (iterator.hasNext()) { + val emitter = iterator.next() + val emitters = emitter.value + if (emitters.canceled) { + dirtyEmitters.remove(emitters.uuid) + val players = filterVisiblePlayer(emitters) + if (players.isEmpty()) { + iterator.remove() + continue + } + val packet = createRemovePacket(emitters) + players.forEach { + val player = emitters.world!!.getPlayerByUUID(it) ?: return@forEach + CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) + visible[it]?.remove(emitters) + } + if (players.isNotEmpty()) { + CooEventBus.call(EmitterRemoveEvent(emitters, false)) + } + iterator.remove() + continue + } + updateClientVisible(emitters) + emitters.tick() + if (dirtyEmitters.remove(emitters.uuid)) { + sendUpdate(emitters) + } + } + } + + fun doTickClient() { + val player = Minecraft.getInstance().player ?: return + if (player.isDeadOrDying) { + clearAllVisible() + return + } + val iterator = clientEmitters.iterator() + while (iterator.hasNext()) { + val emitters = iterator.next().value + emitters.tick() + if (emitters.canceled) { + iterator.remove() + finishClientSystems(emitters) + CooEventBus.call(EmitterRemoveEvent(emitters, true)) + } + } + } + + fun filterVisiblePlayer(group: ParticleEmitters): Set { + val set = HashSet() + visible.forEach { + if (group in it.value) { + set.add(it.key) + } + } + return set + } + + fun clearVisibleFor(player: Player) { + visible.remove(player.uuid) + } + + fun updateClientVisible(emitters: ParticleEmitters) { + CooEventBus.call(EmitterSpawnEvent(emitters, false)) + val server = CooParticlesAPI.serverOrNull ?: return + server.playerList.players.forEach { p -> + val visibleSet = visible.getOrPut(p.uuid) { HashSet() } + if (p.level().dimension() != emitters.world?.dimension()) { + if (emitters in visibleSet) { + removeView(p, emitters) + visibleSet!!.remove(emitters) + } + return@forEach + } + if (p.isDeadOrDying) { + if (emitters in visibleSet) { + removeView(p, emitters) + visibleSet!!.remove(emitters) + } + return@forEach + } + val shouldView = p.position().distanceTo(emitters.pos) <= 256.0 + if (!shouldView) { + if (emitters in visibleSet) { + removeView(p, emitters) + visibleSet.remove(emitters) + } + return@forEach + } + if (emitters in visibleSet) { + return@forEach + } + addView(p, emitters) + visibleSet.add(emitters) + } + } + + internal fun enqueueDirty(emitters: ParticleEmitters) { + if (serverEmitters[emitters.uuid] === emitters && !emitters.canceled) { + dirtyEmitters.add(emitters.uuid) + } + } + + private fun sendUpdate(emitters: ParticleEmitters) { + if (emitters.canceled) { + return + } + val players = filterVisiblePlayer(emitters) + if (players.isEmpty()) { + return + } + val data = encodeEmittersToArray(emitters) + val packet = PacketParticleEmittersS2C( + emitters.getEmittersID(), + emitters.uuid, + data, + PacketParticleEmittersS2C.PacketType.CHANGE + ) + players.forEach { + val player = emitters.world!!.getPlayerByUUID(it) ?: return@forEach + CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) + } + } + + fun sendChange(emitters: ParticleEmitters, to: ServerPlayer) { + val data = encodeEmittersToArray(emitters) + val packet = PacketParticleEmittersS2C( + emitters.getEmittersID(), + emitters.uuid, + data, + PacketParticleEmittersS2C.PacketType.CHANGE + ) + CooParticlesServices.SERVER_NETWORK.send(packet, to) + } + + private fun addView(player: ServerPlayer, emitters: ParticleEmitters) { + val data = encodeEmittersToArray(emitters) + + val packet = PacketParticleEmittersS2C( + emitters.getEmittersID(), + emitters.uuid, + data, + PacketParticleEmittersS2C.PacketType.CREATE + ) + CooParticlesServices.SERVER_NETWORK.send(packet, player) + } + + fun clearAllVisible() { + clientEmitters.values.forEach { + it.remove() + finishClientSystems(it) + CooEventBus.call(EmitterRemoveEvent(it, true)) + } + clientEmitters.clear() + CParticleEmitterBridge.clear() + } + + fun clearServer() { + serverEmitters.onEach { it.value.canceled = true }.clear() + visible.clear() + dirtyEmitters.clear() + } + + private fun removeView(player: ServerPlayer, emitters: ParticleEmitters) { + CooParticlesServices.SERVER_NETWORK.send(createRemovePacket(emitters), player) + CooEventBus.call(EmitterRemoveEvent(emitters, false)) + } + + private fun createRemovePacket(emitters: ParticleEmitters): PacketParticleEmittersS2C { + return PacketParticleEmittersS2C( + emitters.getEmittersID(), + emitters.uuid, + ByteArray(0), + PacketParticleEmittersS2C.PacketType.REMOVE + ) + } + + private fun encodeEmittersToArray(emitters: ParticleEmitters): ByteArray { + val codec = emitters.getCodec() + val buf = PacketByteBuf( + Unpooled.buffer(), + ) + return try { + codec.encode(buf, emitters) + ByteArray(buf.readableBytes()).also { buf.readBytes(it) } + } finally { + buf.release() + } + } + + internal fun init() { + } + + private var handled = false + fun registerScanner() { + if (handled) { + return + } + val start = System.currentTimeMillis() + handled = true + CooParticlesConstants.logger.info("正在自动注册 Emitters") + CooAPIScanner.getWithAnnotation( + CooAutoRegister::class.java + ).forEach { + findListenerHandlers(it) + } + val end = System.currentTimeMillis() + CooParticlesConstants.logger.info("Emitters 注册完成 耗时 ${end - start} ms") + } + + private fun findListenerHandlers(target: SimpleClassInfo) { + val clazz = target.toClass() + if (!ParticleEmitters::class.java.isAssignableFrom(clazz)) { + return + } + if (AutoParticleEmitters::class.java.isAssignableFrom(clazz)) { + @Suppress("UNCHECKED_CAST") + register( + clazz.name, + ParticleEmittersRegistryHelper.generateClassParticleCodec(clazz as Class) + ) + return + } + if (AutoTransformableCParticleEmitter::class.java.isAssignableFrom(clazz)) { + @Suppress("UNCHECKED_CAST") + register( + clazz.name, + ParticleEmittersRegistryHelper.generateTransformableCParticleEmitterCodec( + clazz as Class, + ), + ) + return + } + if (AutoEmitters::class.java.isAssignableFrom(clazz)) { + @Suppress("UNCHECKED_CAST") + register( + clazz.name, + ParticleEmittersRegistryHelper.generateClassEmittersCodec(clazz as Class) + ) + return + } + val instance = + clazz.declaredConstructors.find { + it.parameterCount == 0 + }?.newInstance() ?: clazz.getDeclaredConstructor(Vec3::class.java, Level::class.java) + .newInstance(Vec3.ZERO, null) + register(instance as ParticleEmitters) + } + + private fun finishClientSystems(emitter: ParticleEmitters) { + if (emitter is TransformableCParticleEmitter) { + emitter.finishClientSystems() + } else if (emitter is ClassParticleEmitters) { + CParticleEmitterBridge.finishEmitter(emitter) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt new file mode 100644 index 00000000..157745d3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt @@ -0,0 +1,243 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.cparticle.CParticleColorCurve +import cn.coostack.cooparticlesapi.cparticle.CParticleCurve +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.network.PacketByteBuf +import org.joml.Vector3f +import kotlin.math.roundToInt +import kotlin.random.Random + +class SimpleRandomParticleData { + + companion object { + val PACKET_CODEC: ForgeStreamCodec = ForgeStreamCodec.of({ buf, it -> + buf.apply { + writeInt(it.maxAge) + writeInt(it.minAge) + writeInt(it.maxCount) + writeInt(it.minCount) + writeDouble(it.maxSize) + writeDouble(it.minSize) + writeDouble(it.maxSizeX) + writeDouble(it.minSizeX) + writeDouble(it.maxSizeY) + writeDouble(it.minSizeY) + writeDouble(it.maxSizeZ) + writeDouble(it.minSizeZ) + writeDouble(it.maxSpeed) + writeDouble(it.minSpeed) + writeFloat(it.maxYaw) + writeFloat(it.minYaw) + writeFloat(it.maxPitch) + writeFloat(it.minPitch) + writeFloat(it.maxRoll) + writeFloat(it.minRoll) + writeFloat(it.maxAlpha) + writeFloat(it.minAlpha) + writeVector3f(it.leftColor) + writeVector3f(it.rightColor) + } + }, { + SimpleRandomParticleData().apply { + maxAge = it.readInt() + minAge = it.readInt() + maxCount = it.readInt() + minCount = it.readInt() + maxSize = it.readDouble() + minSize = it.readDouble() + maxSizeX = it.readDouble() + minSizeX = it.readDouble() + maxSizeY = it.readDouble() + minSizeY = it.readDouble() + maxSizeZ = it.readDouble() + minSizeZ = it.readDouble() + maxSpeed = it.readDouble() + minSpeed = it.readDouble() + maxYaw = it.readFloat() + minYaw = it.readFloat() + maxPitch = it.readFloat() + minPitch = it.readFloat() + maxRoll = it.readFloat() + minRoll = it.readFloat() + maxAlpha = it.readFloat() + minAlpha = it.readFloat() + leftColor = it.readVector3f() + rightColor = it.readVector3f() + } + }) + } + + var maxAge = 10 + var minAge = 1 + + var maxCount = 10 + var minCount = 1 + + private var currentMaxSize = 0.3 + var maxSize: Double + get() = currentMaxSize + set(value) { + currentMaxSize = value + maxSizeX = value + maxSizeY = value + maxSizeZ = value + } + + private var currentMinSize = 0.1 + var minSize: Double + get() = currentMinSize + set(value) { + currentMinSize = value + minSizeX = value + minSizeY = value + minSizeZ = value + } + + var maxSizeX = currentMaxSize + var minSizeX = currentMinSize + var maxSizeY = currentMaxSize + var minSizeY = currentMinSize + var maxSizeZ = currentMaxSize + var minSizeZ = currentMinSize + + var minSpeed = 0.1 + var maxSpeed = 1.0 + + var maxYaw = 0f + var minYaw = 0f + var maxPitch = 0f + var minPitch = 0f + var maxRoll = 0f + var minRoll = 0f + + var minAlpha = 1f + var maxAlpha = 1f + + var leftColor = Vector3f(1f, 1f, 1f) + var rightColor = Vector3f(1f, 1f, 1f) + + fun getRandomParticleMaxAge(): Int = if (maxAge > minAge) { + Random.nextInt(minAge, maxAge) + } else minAge + + fun getRandomCount(): Int = if (maxCount > minCount) Random.nextInt(minCount, maxCount) else minCount + fun getRandomSize(): Float = + if (maxSize > minSize) Random.nextDouble(minSize, maxSize).toFloat() else minSize.toFloat() + + fun getRandomSpeed(): Double = if (maxSpeed > minSpeed) Random.nextDouble(minSpeed, maxSpeed) else minSpeed + + fun getRandomSizeX(): Float = getRandomDouble(minSizeX, maxSizeX).toFloat() + fun getRandomSizeY(): Float = getRandomDouble(minSizeY, maxSizeY).toFloat() + fun getRandomSizeZ(): Float = getRandomDouble(minSizeZ, maxSizeZ).toFloat() + fun getRandomYaw(): Float = getRandomFloat(minYaw, maxYaw) + fun getRandomPitch(): Float = getRandomFloat(minPitch, maxPitch) + fun getRandomRoll(): Float = getRandomFloat(minRoll, maxRoll) + fun getRandomAlpha(): Float = getRandomFloat(minAlpha, maxAlpha) + fun getRandomColor(): Vector3f { + return Vector3f( + getRandomBetween(leftColor.x, rightColor.x), + getRandomBetween(leftColor.y, rightColor.y), + getRandomBetween(leftColor.z, rightColor.z), + ) + } + + fun getLinerColorCurve(): CParticleColorCurve = CParticleColorCurve.linear( + leftColor, rightColor + ) + + fun getLinerAlphaCurve() = CParticleCurve.linear(minAlpha, maxAlpha) + + fun getInterpolatedParticleMaxAge(progress: Number): Int = getInterpolatedInt(progress, minAge, maxAge) + fun getInterpolatedCount(progress: Number): Int = getInterpolatedInt(progress, minCount, maxCount) + fun getInterpolatedSize(progress: Number): Float = getInterpolatedDouble(progress, minSize, maxSize).toFloat() + fun getInterpolatedSizeX(progress: Number): Float = getInterpolatedDouble(progress, minSizeX, maxSizeX).toFloat() + fun getInterpolatedSizeY(progress: Number): Float = getInterpolatedDouble(progress, minSizeY, maxSizeY).toFloat() + fun getInterpolatedSizeZ(progress: Number): Float = getInterpolatedDouble(progress, minSizeZ, maxSizeZ).toFloat() + fun getInterpolatedSpeed(progress: Number): Double = getInterpolatedDouble(progress, minSpeed, maxSpeed) + fun getInterpolatedYaw(progress: Number): Float = getInterpolatedFloat(progress, minYaw, maxYaw) + fun getInterpolatedPitch(progress: Number): Float = getInterpolatedFloat(progress, minPitch, maxPitch) + fun getInterpolatedRoll(progress: Number): Float = getInterpolatedFloat(progress, minRoll, maxRoll) + fun getInterpolatedAlpha(progress: Number): Float = getInterpolatedFloat(progress, minAlpha, maxAlpha) + fun getInterpolatedColor(progress: Number): Vector3f { + return GraphMathHelper.lerp(progress.toDouble().coerceIn(0.0, 1.0), leftColor, rightColor) + } + + fun interpolate(progress: Number): ControlableParticleData { + return setupInterpolated(progress, ControlableParticleData()) + } + + fun setupInterpolated(progress: Number, data: ControlableParticleData): ControlableParticleData { + data.maxAge = getInterpolatedParticleMaxAge(progress) + data.speed = getInterpolatedSpeed(progress) + setupSize( + data, + getInterpolatedSizeX(progress), + getInterpolatedSizeY(progress), + getInterpolatedSizeZ(progress) + ) + data.yaw = getInterpolatedYaw(progress) + data.pitch = getInterpolatedPitch(progress) + data.roll = getInterpolatedRoll(progress) + data.alpha = getInterpolatedAlpha(progress) + data.color = getInterpolatedColor(progress) + return data + } + + fun setupRandomly(data: ControlableParticleData): ControlableParticleData { + val (sizeX, sizeY, sizeZ) = if (hasUniformSizeRange()) { + val size = getRandomSizeX() + Triple(size, size, size) + } else { + Triple(getRandomSizeX(), getRandomSizeY(), getRandomSizeZ()) + } + + data.maxAge = getRandomParticleMaxAge() + data.speed = getRandomSpeed() + setupSize(data, sizeX, sizeY, sizeZ) + data.yaw = getRandomYaw() + data.pitch = getRandomPitch() + data.roll = getRandomRoll() + data.alpha = getRandomAlpha() + data.color = getRandomColor() + return data + } + + private fun setupSize(data: ControlableParticleData, sizeX: Float, sizeY: Float, sizeZ: Float) { + data.uniformSize = sizeX == sizeY + data.weightSize = sizeX + data.heightSize = sizeY + data.depthSize = sizeZ + } + + private fun hasUniformSizeRange(): Boolean { + return minSizeX == minSizeY && minSizeY == minSizeZ && + maxSizeX == maxSizeY && maxSizeY == maxSizeZ + } + + private fun getRandomDouble(min: Double, max: Double): Double { + return if (max > min) Random.nextDouble(min, max) else min + } + + private fun getRandomFloat(min: Float, max: Float): Float { + return if (max > min) Random.nextDouble(min.toDouble(), max.toDouble()).toFloat() else min + } + + private fun getRandomBetween(left: Float, right: Float): Float { + val min = minOf(left, right) + val max = maxOf(left, right) + return if (max > min) Random.nextDouble(min.toDouble(), max.toDouble()).toFloat() else left + } + + private fun getInterpolatedInt(progress: Number, min: Int, max: Int): Int { + return GraphMathHelper.lerp(progress.toDouble(), min.toDouble(), max.toDouble()).roundToInt() + } + + private fun getInterpolatedDouble(progress: Number, min: Double, max: Double): Double { + return GraphMathHelper.lerp(progress.toDouble(), min, max) + } + + private fun getInterpolatedFloat(progress: Number, min: Float, max: Float): Float { + return GraphMathHelper.lerp(progress.toDouble(), min, max) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt new file mode 100644 index 00000000..32ebd3d3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt @@ -0,0 +1,233 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.annotations.emitter.handle.ParticleEmittersRegistryHelper +import cn.coostack.cooparticlesapi.cparticle.CParticleSystemManager +import cn.coostack.cooparticlesapi.cparticle.compat.TransformableCParticleEmitterBridge +import cn.coostack.cooparticlesapi.cparticle.force.CParticleForce +import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceSink +import cn.coostack.cooparticlesapi.extend.minus +import cn.coostack.cooparticlesapi.extend.plus +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.GlobalWindDirection +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.WindDirection +import cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind.WindDirections +import cn.coostack.cooparticlesapi.network.particle.emitters.event.ParticleEventHandler +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.interpolator.Interpolator +import cn.coostack.cooparticlesapi.utils.interpolator.emitters.LineEmitterInterpolator +import net.minecraft.client.Minecraft +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Quaternionfc +import java.util.UUID +import kotlin.math.max + +abstract class TransformableCParticleEmitter( + pos: Vec3, + override var world: Level?, +) : ParticleEmitters { + + private val posState = dirty(pos) + override var pos by posState + + override var tick: Int = 0 + + override var maxTick: Int = 120 + + override var delay: Int = 0 + + override var uuid: UUID = UUID.randomUUID() + + override var canceled: Boolean = false + + override var playing: Boolean = false + + var airDensity = 0.0 + var gravity: Double = 0.0 + var mass: Double = 1.0 + + val handlerList = ConcurrentHashMap>() + + var enableInterpolator = false + + var emittersInterpolator: Interpolator = LineEmitterInterpolator() + .setRefiner(5.0) + + var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { + it.loadEmitters(this) + } + + override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + val handlerID = handler.getHandlerID() + if (!ParticleEventHandlerManager.hasRegister(handlerID)) { + ParticleEventHandlerManager.register(handler) + } + val eventID = handler.getTargetEventID() + val list = handlerList.getOrPut(eventID) { TreeMap() } + list[handler] = innerClass + } + + override fun start() { + if (playing) return + playing = true + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + override fun stop() { + canceled = true + } + + override fun tick() { + if (canceled || !playing) return + world ?: return + doTick() + if (!world!!.isClientSide) { + increaseTick() + return + } + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + if (tick % max(1, delay) == 0) { + if (enableInterpolator) { + val res = emittersInterpolator.getRefinedResult() + val count = res.size + res.forEachIndexed { index, relative -> + val current = relative.toVector() + val lerpProgress = index / (count - 1f) + doSubtick(current, lerpProgress) + spawnParticle(current, lerpProgress) + } + } else { + spawnParticle(pos, 1f) + } + } + increaseTick() + } + + private fun increaseTick() { + if (++tick >= maxTick && maxTick != -1) { + stop() + } + } + + override fun spawnParticle(pos: Vec3, lerpProgress: Float) { + if (!world!!.isClientSide) return + val world = world as ClientLevel + val controls = genControls(lerpProgress) + val total = controls.size.coerceAtLeast(1).toFloat() + var spawnedCount = 0f + controls.forEach { (data, relative) -> + spawnedCount++ + val spawnPos = pos.add(relative.toVector()) + val particleLerpProgress = spawnedCount / total + if (!isVisibleToClient(data, spawnPos)) { + return@forEach + } + val control = data.createControler( + world, + spawnPos, + particleLerpProgress, + lerpProgress + ) + val displayed = data.getDisplayer().display(spawnPos, world) ?: control + singleControlableAction( + displayed, + data, + RelativeLocation.of(spawnPos), + world, + particleLerpProgress, + lerpProgress + ) + } + } + + protected open fun isVisibleToClient(data: SerializableData, spawnPos: Vec3): Boolean { + val visibleRange = when (data) { + is ControlableParticleData -> data.visibleRange + is DisplayEntityEmittersData -> data.visibleRange + else -> -1f + } + if (visibleRange < 0f) return true + val player = Minecraft.getInstance().player ?: return false + return player.position().distanceTo(spawnPos) <= visibleRange + } + + abstract fun doTick() + + abstract fun genControls(lerpProgress: Float): List> + + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + abstract fun singleControlableAction( + controler: Controlable<*>, + data: SerializableData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + override fun update(emitters: ParticleEmitters) { + if (emitters !is TransformableCParticleEmitter) return + this.posState.setCodecValue(emitters.pos) + this.world = emitters.world + this.tick = emitters.tick + this.maxTick = emitters.maxTick + this.delay = emitters.delay + this.uuid = emitters.uuid + this.canceled = emitters.canceled + this.playing = emitters.playing + this.handlerList.putAll(emitters.handlerList) + this.emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) + ParticleEmittersRegistryHelper.updateEmitter(this, emitters) + } + + override fun getValue(): ParticleEmitters { + return this + } + + override fun markDirty() { + if (world?.isClientSide != true) { + ParticleEmittersManager.enqueueDirty(this) + } + } + + override fun remove() { + canceled = true + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ClientLevel) return + this.world = world + this.pos = pos + ParticleEmittersManager.spawnEmitters(this) + } + + override fun isValid(): Boolean { + return !canceled + } + + override fun rotateAsAxis(radian: Double) { + } + + override fun rotateToPoint(to: RelativeLocation) { + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + } + + override fun teleportTo(to: Vec3) { + if (pos == to) return + pos = to + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt new file mode 100644 index 00000000..d93dff22 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import net.minecraft.network.PacketByteBuf + +object BallWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt new file mode 100644 index 00000000..07cd620c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import net.minecraft.network.PacketByteBuf + +object BoxWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt new file mode 100644 index 00000000..77d7ce63 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import net.minecraft.network.PacketByteBuf + +object GlobalWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt new file mode 100644 index 00000000..e067abac --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import net.minecraft.network.PacketByteBuf + +interface WindDirection { + fun nextTick(): Double +} + +object BallWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") + } +} + +object BoxWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") + } +} + +object GlobalWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") + } +} + +object WindDirections { + val CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, dir -> + when (dir) { + is BallWindDirection -> buf.writeByte(0) + is BoxWindDirection -> buf.writeByte(1) + is GlobalWindDirection -> buf.writeByte(2) + else -> buf.writeByte(-1) + } + }, + { buf -> + when (buf.readUnsignedByte().toInt()) { + 0 -> BallWindDirection + 1 -> BoxWindDirection + 2 -> GlobalWindDirection + else -> throw IllegalArgumentException("Unknown wind direction type") + } + } + ) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt new file mode 100644 index 00000000..98e04479 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import net.minecraft.network.PacketByteBuf + +object WindDirections { + val CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, dir -> + when (dir) { + is BallWindDirection -> buf.writeByte(0) + is BoxWindDirection -> buf.writeByte(1) + is GlobalWindDirection -> buf.writeByte(2) + else -> buf.writeByte(-1) + } + }, + { buf -> + when (buf.readUnsignedByte().toInt()) { + 0 -> BallWindDirection + 1 -> BoxWindDirection + 2 -> GlobalWindDirection + else -> throw IllegalArgumentException("Unknown wind direction type") + } + } + ) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt new file mode 100644 index 00000000..a59f8275 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +object BoxEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt new file mode 100644 index 00000000..119e77bc --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt @@ -0,0 +1,54 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +interface EmittersShootType { + fun nextShoot(): Pair +} + +object BoxEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} + +object LineEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} + +object PointEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} + +object MathEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} + +object EmittersShootTypes { + val CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, type -> + when (type) { + is BoxEmittersShootType -> buf.writeByte(0) + is LineEmittersShootType -> buf.writeByte(1) + is PointEmittersShootType -> buf.writeByte(2) + is MathEmittersShootType -> buf.writeByte(3) + else -> buf.writeByte(-1) + } + }, + { buf -> + when (buf.readUnsignedByte().toInt()) { + 0 -> BoxEmittersShootType + 1 -> LineEmittersShootType + 2 -> PointEmittersShootType + 3 -> MathEmittersShootType + else -> throw IllegalArgumentException("Unknown shoot type") + } + } + ) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt new file mode 100644 index 00000000..b4ccc6a5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt @@ -0,0 +1,27 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +object EmittersShootTypes { + val CODEC: ForgeStreamCodec = + cn.coostack.cooparticlesapi.annotations.codec.ForgeStreamCodec.of( + { buf, type -> + when (type) { + is BoxEmittersShootType -> buf.writeByte(0) + is LineEmittersShootType -> buf.writeByte(1) + is PointEmittersShootType -> buf.writeByte(2) + is MathEmittersShootType -> buf.writeByte(3) + else -> buf.writeByte(-1) + } + }, + { buf -> + when (buf.readUnsignedByte().toInt()) { + 0 -> BoxEmittersShootType + 1 -> LineEmittersShootType + 2 -> PointEmittersShootType + 3 -> MathEmittersShootType + else -> throw IllegalArgumentException("Unknown shoot type") + } + } + ) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt new file mode 100644 index 00000000..0135d085 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +object LineEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt new file mode 100644 index 00000000..fa01dc5f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +object MathEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt new file mode 100644 index 00000000..a2da4dd7 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +object PointEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt new file mode 100644 index 00000000..298e6fb5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt @@ -0,0 +1,14 @@ +package cn.coostack.cooparticlesapi.particles + +import cn.coostack.cooparticlesapi.particles.impl.ControlableCloudEffect +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleOptions +import java.util.UUID + +abstract class ControlableParticleEffect(var controlUUID: UUID, val faceToPlayer: Boolean = true) : ParticleOptions { + abstract fun getPacketCodec(): ForgeStreamCodec + abstract fun clone(): ControlableParticleEffect +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt new file mode 100644 index 00000000..0b7abac2 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt @@ -0,0 +1,330 @@ +package cn.coostack.cooparticlesapi.particles + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.particles.impl.* +import cn.coostack.cooparticlesapi.platform.registry.CommonDeferredRegistry +import com.mojang.serialization.MapCodec +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.core.particles.ParticleType +import net.minecraft.core.registries.BuiltInRegistries + + + +import net.minecraft.resources.ResourceLocation + +object CooModParticles { + val particleTypes = mutableListOf>>() + val controlableEndRod = register( + "controlable_end_rod", false, { ControlableEndRodEffect.codec } + ) + + val controlableEnchantment = register( + "controlable_enchantment", + false, + { ControlableEnchantmentEffect.codec }, + { ControlableEnchantmentEffect.packetCode } + ) + + val controlableCloud = register( + "controlable_cloud", false, { ControlableCloudEffect.codec } + ) + + val controlableFlash = register( + "controlable_flash", false, { ControlableFlashEffect.codec } + ) + + val controlableFirework = register( + "controlable_firework", false, { ControlableFireworkEffect.codec } + ) + + val controlableFallingDust = register( + "controlable_falling_dust", + false, + { ControlableFallingDustEffect.codec }, + { ControlableFallingDustEffect.packetCode } + ) + + val controlableSplash = register( + "controlable_splash", + false, + { ControlableSplashEffect.codec }, + { ControlableSplashEffect.packetCode } + ) + + val controlableAngryVillager = register( + "controlable_angry_villager", false, { ControlableAngryVillagerEffect.codec } + ) + val controlableBubble = register( + "controlable_bubble", false, { ControlableBubbleEffect.codec } + ) + val controlableBubbleColumnUp = register( + "controlable_bubble_column_up", false, { ControlableBubbleColumnUpEffect.codec } + ) + val controlableBubblePop = register( + "controlable_bubble_pop", false, { ControlableBubblePopEffect.codec } + ) + val controlableCampfireCosySmoke = register( + "controlable_campfire_cosy_smoke", true, { ControlableCampfireCosySmokeEffect.codec } + ) + val controlableCampfireSignalSmoke = register( + "controlable_campfire_signal_smoke", true, { ControlableCampfireSignalSmokeEffect.codec } + ) + val controlableComposter = register( + "controlable_composter", false, { ControlableComposterEffect.codec } + ) + val controlableCrit = register( + "controlable_crit", false, { ControlableCritEffect.codec } + ) + val controlableCurrentDown = register( + "controlable_current_down", false, { ControlableCurrentDownEffect.codec } + ) + val controlableDamageIndicator = register( + "controlable_damage_indicator", true, { ControlableDamageIndicatorEffect.codec } + ) + val controlableDragonBreath = register( + "controlable_dragon_breath", false, { ControlableDragonBreathEffect.codec } + ) + val controlableDolphin = register( + "controlable_dolphin", false, { ControlableDolphinEffect.codec } + ) + val controlableDrippingLava = register( + "controlable_dripping_lava", false, { ControlableDrippingLavaEffect.codec } + ) + val controlableFallingLava = register( + "controlable_falling_lava", false, { ControlableFallingLavaEffect.codec } + ) + val controlableLandingLava = register( + "controlable_landing_lava", false, { ControlableLandingLavaEffect.codec } + ) + val controlableDrippingWater = register( + "controlable_dripping_water", false, { ControlableDrippingWaterEffect.codec } + ) + val controlableFallingWater = register( + "controlable_falling_water", false, { ControlableFallingWaterEffect.codec } + ) + val controlableEffect = register( + "controlable_effect", false, { ControlableEffectParticleEffect.codec } + ) + val controlableEnchantedHit = register( + "controlable_enchanted_hit", false, { ControlableEnchantedHitEffect.codec } + ) + val controlableExplosion = register( + "controlable_explosion", true, { ControlableExplosionEffect.codec } + ) + val controlableSonicBoom = register( + "controlable_sonic_boom", true, { ControlableSonicBoomEffect.codec } + ) + val controlableGust = register( + "controlable_gust", true, { ControlableGustEffect.codec } + ) + val controlableSmallGust = register( + "controlable_small_gust", false, { ControlableSmallGustEffect.codec } + ) + val controlableFishing = register( + "controlable_fishing", false, { ControlableFishingEffect.codec } + ) + val controlableFlame = register( + "controlable_flame", false, { ControlableFlameEffect.codec } + ) + val controlableInfested = register( + "controlable_infested", false, { ControlableInfestedEffect.codec } + ) + val controlableCherryLeaves = register( + "controlable_cherry_leaves", false, { ControlableCherryLeavesEffect.codec } + ) + val controlableSculkSoul = register( + "controlable_sculk_soul", false, { ControlableSculkSoulEffect.codec } + ) + val controlableSculkChargePop = register( + "controlable_sculk_charge_pop", true, { ControlableSculkChargePopEffect.codec } + ) + val controlableSoul = register( + "controlable_soul", false, { ControlableSoulEffect.codec } + ) + val controlableSoulFireFlame = register( + "controlable_soul_fire_flame", false, { ControlableSoulFireFlameEffect.codec } + ) + val controlableHappyVillager = register( + "controlable_happy_villager", false, { ControlableHappyVillagerEffect.codec } + ) + val controlableHeart = register( + "controlable_heart", false, { ControlableHeartEffect.codec } + ) + val controlableInstantEffect = register( + "controlable_instant_effect", false, { ControlableInstantEffectParticleEffect.codec } + ) + val controlableLargeSmoke = register( + "controlable_large_smoke", false, { ControlableLargeSmokeEffect.codec } + ) + val controlableLava = register( + "controlable_lava", false, { ControlableLavaEffect.codec } + ) + val controlableMycelium = register( + "controlable_mycelium", false, { ControlableMyceliumEffect.codec } + ) + val controlableNautilus = register( + "controlable_nautilus", false, { ControlableNautilusEffect.codec } + ) + val controlableNote = register( + "controlable_note", false, { ControlableNoteEffect.codec } + ) + val controlablePoof = register( + "controlable_poof", true, { ControlablePoofEffect.codec } + ) + val controlablePortal = register( + "controlable_portal", false, { ControlablePortalEffect.codec } + ) + val controlableRain = register( + "controlable_rain", false, { ControlableRainEffect.codec } + ) + val controlableSmoke = register( + "controlable_smoke", false, { ControlableSmokeEffect.codec } + ) + val controlableWhiteSmoke = register( + "controlable_white_smoke", false, { ControlableWhiteSmokeEffect.codec } + ) + val controlableSneeze = register( + "controlable_sneeze", false, { ControlableSneezeEffect.codec } + ) + val controlableSnowflake = register( + "controlable_snowflake", false, { ControlableSnowflakeEffect.codec } + ) + val controlableSpit = register( + "controlable_spit", true, { ControlableSpitEffect.codec } + ) + val controlableSweepAttack = register( + "controlable_sweep_attack", true, { ControlableSweepAttackEffect.codec } + ) + val controlableTotemOfUndying = register( + "controlable_totem_of_undying", false, { ControlableTotemOfUndyingEffect.codec } + ) + val controlableSquidInk = register( + "controlable_squid_ink", true, { ControlableSquidInkEffect.codec } + ) + val controlableUnderwater = register( + "controlable_underwater", false, { ControlableUnderwaterEffect.codec } + ) + val controlableWitch = register( + "controlable_witch", false, { ControlableWitchEffect.codec } + ) + val controlableDrippingHoney = register( + "controlable_dripping_honey", false, { ControlableDrippingHoneyEffect.codec } + ) + val controlableFallingHoney = register( + "controlable_falling_honey", false, { ControlableFallingHoneyEffect.codec } + ) + val controlableLandingHoney = register( + "controlable_landing_honey", false, { ControlableLandingHoneyEffect.codec } + ) + val controlableFallingNectar = register( + "controlable_falling_nectar", false, { ControlableFallingNectarEffect.codec } + ) + val controlableFallingSporeBlossom = register( + "controlable_falling_spore_blossom", false, { ControlableFallingSporeBlossomEffect.codec } + ) + val controlableSporeBlossomAir = register( + "controlable_spore_blossom_air", false, { ControlableSporeBlossomAirEffect.codec } + ) + val controlableAsh = register( + "controlable_ash", false, { ControlableAshEffect.codec } + ) + val controlableCrimsonSpore = register( + "controlable_crimson_spore", false, { ControlableCrimsonSporeEffect.codec } + ) + val controlableWarpedSpore = register( + "controlable_warped_spore", false, { ControlableWarpedSporeEffect.codec } + ) + val controlableDrippingObsidianTear = register( + "controlable_dripping_obsidian_tear", false, { ControlableDrippingObsidianTearEffect.codec } + ) + val controlableFallingObsidianTear = register( + "controlable_falling_obsidian_tear", false, { ControlableFallingObsidianTearEffect.codec } + ) + val controlableLandingObsidianTear = register( + "controlable_landing_obsidian_tear", false, { ControlableLandingObsidianTearEffect.codec } + ) + val controlableReversePortal = register( + "controlable_reverse_portal", false, { ControlableReversePortalEffect.codec } + ) + val controlableWhiteAsh = register( + "controlable_white_ash", false, { ControlableWhiteAshEffect.codec } + ) + val controlableSmallFlame = register( + "controlable_small_flame", false, { ControlableSmallFlameEffect.codec } + ) + val controlableDrippingDripstoneWater = register( + "controlable_dripping_dripstone_water", false, { ControlableDrippingDripstoneWaterEffect.codec } + ) + val controlableFallingDripstoneWater = register( + "controlable_falling_dripstone_water", false, { ControlableFallingDripstoneWaterEffect.codec } + ) + val controlableDrippingDripstoneLava = register( + "controlable_dripping_dripstone_lava", false, { ControlableDrippingDripstoneLavaEffect.codec } + ) + val controlableFallingDripstoneLava = register( + "controlable_falling_dripstone_lava", false, { ControlableFallingDripstoneLavaEffect.codec } + ) + val controlableGlowSquidInk = register( + "controlable_glow_squid_ink", true, { ControlableGlowSquidInkEffect.codec } + ) + val controlableGlow = register( + "controlable_glow", true, { ControlableGlowEffect.codec } + ) + val controlableWaxOn = register( + "controlable_wax_on", true, { ControlableWaxOnEffect.codec } + ) + val controlableWaxOff = register( + "controlable_wax_off", true, { ControlableWaxOffEffect.codec } + ) + val controlableElectricSpark = register( + "controlable_electric_spark", true, { ControlableElectricSparkEffect.codec } + ) + val controlableScrape = register( + "controlable_scrape", true, { ControlableScrapeEffect.codec } + ) + val controlableEggCrack = register( + "controlable_egg_crack", false, { ControlableEggCrackEffect.codec } + ) + val controlableDustPlume = register( + "controlable_dust_plume", false, { ControlableDustPlumeEffect.codec } + ) + val controlableTrialSpawnerDetection = register( + "controlable_trial_spawner_detection", true, { ControlableTrialSpawnerDetectionEffect.codec } + ) + val controlableTrialSpawnerDetectionOminous = register( + "controlable_trial_spawner_detection_ominous", true, { ControlableTrialSpawnerDetectionOminousEffect.codec } + ) + val controlableVaultConnection = register( + "controlable_vault_connection", true, { ControlableVaultConnectionEffect.codec } + ) + val controlableRaidOmen = register( + "controlable_raid_omen", false, { ControlableRaidOmenEffect.codec } + ) + val controlableTrialOmen = register( + "controlable_trial_omen", false, { ControlableTrialOmenEffect.codec } + ) + val controlableOminousSpawning = register( + "controlable_ominous_spawning", true, { ControlableOminousSpawningEffect.codec } + ) + + fun reg() { + } + + fun register( + id: String, alwaysShow: Boolean, + codecGetter: (type: ParticleType) -> MapCodec, + ): CommonDeferredRegistry> { + val registry = CommonDeferredRegistry( + BuiltInRegistries.PARTICLE_TYPE, + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, id) + ) { + object : ParticleType(alwaysShow) { + override fun codec(): MapCodec { + return codecGetter(this) + } + } + } + particleTypes.add(registry) + return registry as CommonDeferredRegistry> + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ParticleCameraOption.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ParticleCameraOption.kt new file mode 100644 index 00000000..7d367f41 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ParticleCameraOption.kt @@ -0,0 +1,49 @@ +package cn.coostack.cooparticlesapi.particles + +import com.mojang.serialization.Codec +import net.minecraft.network.PacketByteBuf + +enum class ParticleCameraOption( + val enableAxis: Boolean, + val enableYaw: Boolean, + val enablePitch: Boolean, + val enableRoll: Boolean +) { + BILLBOARD( + enableAxis = false, + enableYaw = false, + enablePitch = false, + enableRoll = true + ), + AXIS_BILLBOARD( + enableAxis = true, + enableYaw = false, + enablePitch = false, + enableRoll = true + ), + ROTATION( + enableAxis = false, + enableYaw = true, + enablePitch = true, + enableRoll = true + ); + + companion object { + @JvmStatic + val CODEC: Codec = Codec.STRING.xmap( + { value -> entries.firstOrNull { it.name.equals(value, ignoreCase = true) } ?: BILLBOARD }, + { value -> value.name.lowercase() } + ) + + @JvmStatic + val STREAM_CODEC: ForgeStreamCodec = ForgeStreamCodec.of( + { buf, value -> buf.writeEnum(value) }, + { buf -> buf.readEnum(ParticleCameraOption::class.java) } + ) + + @JvmStatic + fun fromFaceToCamera(faceToCamera: Boolean): ParticleCameraOption { + return if (faceToCamera) BILLBOARD else ROTATION + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableCloudEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableCloudEffect.kt new file mode 100644 index 00000000..0fa52645 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableCloudEffect.kt @@ -0,0 +1,61 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.network.PacketByteBuf +import java.util.UUID + +class ControlableCloudEffect(controlUUID: UUID, faceToPlayer: Boolean = true) : ControlableParticleEffect( + controlUUID, + faceToPlayer +) { + companion object { + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(it) { buf, b -> + ControlableCloudEffect( + UUID.fromString( + String(buf.array()) + ), b + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableCloudEffect> = CommonCommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, { + ControlableCloudEffect(it.readUUID(), it.readBoolean()) + } + ) + } + + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableCloud.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableParticleEffect> { + return packetCode + } + + override fun clone(): ControlableParticleEffect { + return ControlableCloudEffect(controlUUID, faceToPlayer) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEnchantmentEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEnchantmentEffect.kt new file mode 100644 index 00000000..3b5fa904 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEnchantmentEffect.kt @@ -0,0 +1,59 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.network.PacketByteBuf +import java.util.UUID + +class ControlableEnchantmentEffect(controlUUID: UUID, faceToPlayer: Boolean = true) : + ControlableParticleEffect(controlUUID, faceToPlayer) { + companion object { + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(it) { buf, b -> + ControlableEnchantmentEffect( + UUID.fromString( + String(buf.array()) + ), b + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableEnchantmentEffect> = CommonCommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, { + ControlableEnchantmentEffect(it.readUUID(), it.readBoolean()) + } + ) + } + + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableEnchantment.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableParticleEffect> { + return packetCode + } + + override fun clone(): ControlableParticleEffect { + return ControlableEnchantmentEffect(controlUUID, faceToPlayer) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEndRodEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEndRodEffect.kt new file mode 100644 index 00000000..6835edfe --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEndRodEffect.kt @@ -0,0 +1,61 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.network.PacketByteBuf +import java.util.UUID + +class ControlableEndRodEffect(controlUUID: UUID, faceToPlayer: Boolean = true) : + ControlableParticleEffect(controlUUID, faceToPlayer) { + companion object { + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, + Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(it) { buf, faceToPlayer -> + ControlableEndRodEffect( + UUID.fromString( + String(buf.array()) + ), faceToPlayer + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableEndRodEffect> = CommonCommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, { + ControlableEndRodEffect(it.readUUID(), it.readBoolean()) + } + ) + } + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableEndRod.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableParticleEffect> { + return packetCode + } + + override fun clone(): ControlableParticleEffect { + return ControlableEndRodEffect( + controlUUID, faceToPlayer + ) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFallingDustEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFallingDustEffect.kt new file mode 100644 index 00000000..759cec6b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFallingDustEffect.kt @@ -0,0 +1,98 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.cparticle.CParticleTextureSource +import cn.coostack.cooparticlesapi.cparticle.CParticleTextureSourceProvider +import cn.coostack.cooparticlesapi.cparticle.textureOfBlock +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import java.util.* + +/** + * 使用 [state] 模型 particle icon 的可控制方块尘效果。 + * + * Example: GPU emitter 会通过 [cparticleTextureSource] 把它放入方块图集批次。 + * Forbidden: 不要在效果对象中直接查询模型或保存 stitch 后的 UV。 + * + * @property state 提供 particle icon、方块染色和随机裁剪范围的方块状态 + */ +class ControlableFallingDustEffect(controlUUID: UUID, val state: BlockState, faceToPlayer: Boolean = true) : + ControlableParticleEffect(controlUUID, faceToPlayer), CParticleTextureSourceProvider { + companion object { + @JvmStatic + val BLOCK_STATE_CODEC = Codec + .withAlternative( + BlockState.CODEC, + BuiltInRegistries.BLOCK.byNameCodec(), + Block::defaultBlockState + ) + + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, + Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + }, + BLOCK_STATE_CODEC.fieldOf("state").forGetter { effect -> effect.state } + ).apply(it) { buf, faceToPlayer, state -> + ControlableFallingDustEffect( + UUID.fromString( + String(buf.array()) + ), state, faceToPlayer + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableFallingDustEffect> = CommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + val id = net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.getId(effect.state); buf.writeVarInt(id) + }, { + val uuid = it.readUUID() + val faceTo = it.readBoolean() + val id = it.readVarInt(); net.minecraft.core.registries.BuiltInRegistries.BLOCK_STATE_REGISTRY.byId(id) ?: net.minecraft.world.level.block.Blocks.AIR.defaultBlockState() + ControlableFallingDustEffect(uuid, state, faceTo) + } + ) + } + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableFallingDust.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableFallingDustEffect> { + return packetCode + } + + /** + * 返回与 CPU FallingDust 相同的方块外观来源。 + * + * Example: 默认配置会应用随机 1/4 裁剪、BlockColors 和 `0.6` 亮度倍率。 + * Forbidden: 此处只描述来源,不解析客户端模型。 + * + * @return 当前 [state] 对应的通用 BlockState 纹理来源 + */ + override fun cparticleTextureSource(): CParticleTextureSource = textureOfBlock(state) + + override fun clone(): ControlableFallingDustEffect { + return ControlableFallingDustEffect( + controlUUID, state, faceToPlayer + ) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFireworkEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFireworkEffect.kt new file mode 100644 index 00000000..972b5638 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFireworkEffect.kt @@ -0,0 +1,61 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.network.PacketByteBuf +import java.util.UUID + +class ControlableFireworkEffect(controlUUID: UUID, faceToPlayer: Boolean = true) : + ControlableParticleEffect(controlUUID, faceToPlayer) { + companion object { + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(it) { buf, b -> + ControlableFireworkEffect( + UUID.fromString( + String(buf.array()) + ), b + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableFireworkEffect> = CommonCommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, { + ControlableFireworkEffect(it.readUUID(), it.readBoolean()) + } + ) + } + + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableFirework.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableParticleEffect> { + return packetCode + } + + override fun clone(): ControlableParticleEffect { + return ControlableFireworkEffect( + controlUUID, faceToPlayer + ) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFlashEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFlashEffect.kt new file mode 100644 index 00000000..d8be945f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFlashEffect.kt @@ -0,0 +1,61 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.network.PacketByteBuf +import java.util.UUID + +class ControlableFlashEffect(controlUUID: UUID, faceToPlayer: Boolean = true) : + ControlableParticleEffect(controlUUID, faceToPlayer) { + companion object { + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(it) { buf, b -> + ControlableFlashEffect( + UUID.fromString( + String(buf.array()) + ), b + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableFlashEffect> = CommonCommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, { + ControlableFlashEffect(it.readUUID(), it.readBoolean()) + } + ) + } + + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableFlash.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableParticleEffect> { + return packetCode + } + + override fun clone(): ControlableParticleEffect { + return ControlableFlashEffect( + controlUUID, faceToPlayer + ) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableSplashEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableSplashEffect.kt new file mode 100644 index 00000000..de7c9431 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableSplashEffect.kt @@ -0,0 +1,61 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import cn.coostack.cooparticlesapi.particles.CooModParticles +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import net.minecraft.network.PacketByteBuf +import java.util.UUID + +class ControlableSplashEffect(controlUUID: UUID, faceToPlayer: Boolean = true) : ControlableParticleEffect( + controlUUID, + faceToPlayer +) { + companion object { + @JvmStatic + val codec: MapCodec = RecordCodecBuilder.mapCodec { + return@mapCodec it.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val toString = effect.controlUUID.toString() + val buffer = Unpooled.buffer() + buffer.writeBytes(toString.toByteArray()) + buffer.nioBuffer() + }, Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(it) { buf, b -> + ControlableSplashEffect( + UUID.fromString( + String(buf.array()) + ), b + ) + } + } + + @JvmStatic + val packetCode: CommonStreamCodec< ControlableSplashEffect> = CommonCommonStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, { + ControlableSplashEffect(it.readUUID(), it.readBoolean()) + } + ) + } + + + override fun getType(): ParticleType<*> { + return CooModParticles.controlableSplash.get() + } + + override fun getPacketCodec(): CommonStreamCodec< out ControlableParticleEffect> { + return packetCode + } + + override fun clone(): ControlableParticleEffect { + return ControlableSplashEffect(controlUUID, faceToPlayer) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt new file mode 100644 index 00000000..b5cb9af4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt @@ -0,0 +1,67 @@ +package cn.coostack.cooparticlesapi.particles.impl + +import cn.coostack.cooparticlesapi.particles.ControlableParticleEffect +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.netty.buffer.Unpooled +import net.minecraft.core.particles.ParticleType +import java.util.UUID + +abstract class SimpleControlableParticleEffect( + controlUUID: UUID, + faceToPlayer: Boolean = true, + private val particleTypeGetter: () -> ParticleType<*>, + private val effectFactory: (UUID, Boolean) -> ControlableParticleEffect, + private val packetCodecGetter: () -> ForgeStreamCodec +) : ControlableParticleEffect(controlUUID, faceToPlayer) { + override fun getType(): ParticleType<*> { + return particleTypeGetter() + } + + override fun getPacketCodec(): ForgeStreamCodec { + return packetCodecGetter() + } + + override fun clone(): ControlableParticleEffect { + return effectFactory(controlUUID, faceToPlayer) + } +} + +internal object SimpleControlableParticleEffectCodecs { + fun mapCodec(factory: (UUID, Boolean) -> T): MapCodec { + return RecordCodecBuilder.mapCodec { instance -> + return@mapCodec instance.group( + Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect -> + val buffer = Unpooled.buffer() + buffer.writeBytes(effect.controlUUID.toString().toByteArray(Charsets.UTF_8)) + buffer.nioBuffer() + }, + Codec.BOOL.fieldOf("face_to_player").forGetter { effect -> + effect.faceToPlayer + } + ).apply(instance) { buf, faceToPlayer -> + val bytes = ByteArray(buf.remaining()) + buf.get(bytes) + factory(UUID.fromString(String(bytes, Charsets.UTF_8)), faceToPlayer) + } + } + } + + fun packetCodec(factory: (UUID, Boolean) -> T): ForgeStreamCodec { + return ForgeStreamCodec.of( + { buf, effect -> + buf.writeUUID(effect.controlUUID) + buf.writeBoolean(effect.faceToPlayer) + }, + { buf -> factory(buf.readUUID(), buf.readBoolean()) } + ) + } +} + +open class SimpleControlableParticleEffectCodecProvider( + factory: (UUID, Boolean) -> T +) { + val codec: MapCodec = SimpleControlableParticleEffectCodecs.mapCodec(factory) + val packetCode: ForgeStreamCodec = SimpleControlableParticleEffectCodecs.packetCodec(factory) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeClientNetworking.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeClientNetworking.kt new file mode 100644 index 00000000..e4d36889 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeClientNetworking.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.platform + +import cn.coostack.cooparticlesapi.network.packet.api.CooClientPacketManager +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.CooPacketEnvelopeC2S + +class ForgeClientNetworking : ClientNetworking { + override fun send(packet: CooPacket) { + CooClientPacketManager.sendTo(packet) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt new file mode 100644 index 00000000..5ae2f74c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt @@ -0,0 +1,61 @@ +package cn.coostack.cooparticlesapi.platform + +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeC2S +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeS2C +import net.minecraft.resources.ResourceLocation +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.ChunkPos +import net.minecraftforge.network.NetworkEvent +import net.minecraftforge.network.simple.SimpleChannel + +object ForgeNetworkChannel { + val channel: SimpleChannel = net.minecraftforge.network.NetworkRegistry.newSimpleChannel( + ResourceLocation(CooParticlesConstants.MOD_ID, "main"), + { true }, + { true }, + { true } + ) + private var nextId = 0 + + fun registerEnvelopeS2C(handler: (CooPacketEnvelopeS2C) -> Unit = {}) { + channel.registerMessage(nextId++, CooPacketEnvelopeS2C::class.java, + { packet, buf -> CooPacketEnvelopeS2C.write(buf, packet) }, + { buf -> CooPacketEnvelopeS2C.read(buf) }, + { packet, ctx -> + handler(packet) + ctx.packetHandled = true + } + ) + } + + fun registerEnvelopeC2S(handler: (CooPacketEnvelopeC2S, ServerPlayer) -> Unit = { _, _ -> }) { + channel.registerMessage(nextId++, CooPacketEnvelopeC2S::class.java, + { packet, buf -> CooPacketEnvelopeC2S.write(buf, packet) }, + { buf -> CooPacketEnvelopeC2S.read(buf) }, + { packet, ctx -> + if (ctx.sender != null) { + handler(packet, ctx.sender) + } + ctx.packetHandled = true + } + ) + } + + fun sendEnvelopeS2CTo(envelope: CooPacketEnvelopeS2C, player: ServerPlayer) { + channel.sendTo(player, envelope) + } + + fun sendEnvelopeS2CToAll(envelope: CooPacketEnvelopeS2C) { + channel.sendToAll(envelope) + } + + fun sendEnvelopeS2CToTrackingChunk(envelope: CooPacketEnvelopeS2C, world: ServerLevel, chunk: ChunkPos) { + val players = world.getChunkSource().chunkMap.getPlayers(chunk.x, chunk.z, false) + players.forEach { channel.sendTo(it as ServerPlayer, envelope) } + } + + fun sendEnvelopeC2S(packet: CooPacketEnvelopeC2S) { + channel.sendToServer(packet) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgePlatformHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgePlatformHelper.kt new file mode 100644 index 00000000..b508b91f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgePlatformHelper.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.platform + +import cn.coostack.cooparticlesapi.platform.services.IPlatformHelper + +class ForgePlatformHelper : IPlatformHelper { + override fun getPlatformName(): String { + return "Forge" + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeRegistry.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeRegistry.kt new file mode 100644 index 00000000..e385cc8f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeRegistry.kt @@ -0,0 +1,52 @@ +package cn.coostack.cooparticlesapi.platform + +import cn.coostack.cooparticlesapi.platform.registry.CommonDeferredRegistry +import net.minecraft.core.Registry +import net.minecraftforge.eventbus.api.IEventBus +import net.minecraftforge.registries.DeferredRegister +import java.util.function.Supplier + +class ForgeRegistry : CooRegistry { + private val registers = HashMap, MutableMap>>() + private var eventBus: IEventBus? = null + + @Synchronized + override fun register(registry: CommonDeferredRegistry): CommonDeferredRegistry { + val registerer = getOrCreateRegister(registry.type, registry.id.namespace) + registerEntry(registerer, registry) + return registry + } + + @Synchronized + override fun init(any: Any?) { + any as IEventBus + if (eventBus != null) return + eventBus = any + registers.values + .flatMap { it.values } + .forEach { it.register(any) } + } + + @Suppress("UNCHECKED_CAST") + private fun getOrCreateRegister( + registry: Registry, + namespace: String + ): DeferredRegister { + val registersByNamespace = registers.getOrPut(registry) { HashMap() } + val registerer = (registersByNamespace[namespace] + ?: DeferredRegister.create(registry, namespace).also { deferred -> + registersByNamespace[namespace] = deferred + eventBus?.let(deferred::register) + }) as DeferredRegister + return registerer + } + + @Suppress("UNCHECKED_CAST") + private fun registerEntry( + registerer: DeferredRegister<*>, + registry: CommonDeferredRegistry<*> + ) { + (registerer as DeferredRegister) + .register(registry.id.path, Supplier { registry.get() }) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeServerNetworking.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeServerNetworking.kt new file mode 100644 index 00000000..f57d87bd --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeServerNetworking.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.platform + +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.CooPacketEnvelopeS2C +import cn.coostack.cooparticlesapi.network.packet.api.CooServerPacketManager +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.ChunkPos + +class ForgeServerNetworking : ServerNetworking { + override fun send(packet: CooPacket, to: ServerPlayer) { + val data = CooPacketRegistry.encode(packet) + val envelope = CooPacketEnvelopeS2C( + kindId = 0, + packetId = packet.id(), + correlationId = 0, + timeoutTicks = 0, + data = data, + ) + ForgeNetworkChannel.INSTANCE.sendEnvelopeS2CTo(envelope, to) + } + + override fun sendAllPlayers(packet: CooPacket) { + val data = CooPacketRegistry.encode(packet) + val envelope = CooPacketEnvelopeS2C( + kindId = 0, + packetId = packet.id(), + correlationId = 0, + timeoutTicks = 0, + data = data, + ) + ForgeNetworkChannel.INSTANCE.sendEnvelopeS2CToAll(envelope) + } + + override fun sendToPlayersTrackingChunk(world: ServerLevel, chunk: ChunkPos, packet: CooPacket) { + val data = CooPacketRegistry.encode(packet) + val envelope = CooPacketEnvelopeS2C( + kindId = 0, + packetId = packet.id(), + correlationId = 0, + timeoutTicks = 0, + data = data, + ) + ForgeNetworkChannel.INSTANCE.sendEnvelopeS2CToTrackingChunk(envelope, world, chunk) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeClientContext.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeClientContext.kt new file mode 100644 index 00000000..35866bd8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeClientContext.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.platform.network + +import net.minecraft.client.Minecraft + +class ForgeClientContext( + val packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket, + val kind: cn.coostack.cooparticlesapi.network.packet.api.CooPacketKind, + val correlationId: Long, + val timeoutTicks: Int, +) : cn.coostack.cooparticlesapi.platform.network.ClientContext { + override fun player(): net.minecraft.world.entity.player.Player = Minecraft.getInstance().player!! + override fun client(): Minecraft = Minecraft.getInstance() +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeServerContext.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeServerContext.kt new file mode 100644 index 00000000..d0019bda --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeServerContext.kt @@ -0,0 +1,19 @@ +package cn.coostack.cooparticlesapi.platform.network + +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.CooServerPacketManager +import net.minecraft.server.level.ServerPlayer + +class ForgeServerContext( + val sender: ServerPlayer, + val packet: cn.coostack.cooparticlesapi.network.packet.api.CooPacket, + val kind: cn.coostack.cooparticlesapi.network.packet.api.CooPacketKind, + val correlationId: Long, + val timeoutTicks: Int, +) : cn.coostack.cooparticlesapi.platform.network.ServerContext { + override fun player(): net.minecraft.world.entity.player.Player = sender + override fun server(): net.minecraft.server.MinecraftServer = sender.server + override fun reply(packet: CooPacket) { + CooServerPacketManager.replyInternal(sender, packet, correlationId) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/services/IPlatformHelper.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/services/IPlatformHelper.kt new file mode 100644 index 00000000..2f8ab234 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/services/IPlatformHelper.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.platform.services + +import net.minecraft.server.level.ServerPlayer + +interface IPlatformHelper { + fun getPlatformName(): String +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt new file mode 100644 index 00000000..b6bba574 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.renderer + +import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper +import cn.coostack.cooparticlesapi.annotations.renderer.handle.RenderEntityRegistryHelper +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoRenderEntity(world: Level?, pos: Vec3 = Vec3.ZERO) : RenderEntity(world, pos) { + override fun getCodec(): ForgeStreamCodec { + return RenderEntityRegistryHelper.generateCodec(this) + } + + override fun loadProfileFromEntity(another: RenderEntity) { + super.loadProfileFromEntity(another) + CodecHelper.updateFields(this, another) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt new file mode 100644 index 00000000..69691b1e --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt @@ -0,0 +1,232 @@ +package cn.coostack.cooparticlesapi.renderer + +import cn.coostack.cooparticlesapi.api.controler.Tickable +import cn.coostack.cooparticlesapi.network.packet.server.PacketRenderEntityS2C +import cn.coostack.cooparticlesapi.api.controler.server.ServerControler +import cn.coostack.cooparticlesapi.renderer.server.ServerRenderEntityManager +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import io.netty.buffer.Unpooled +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.server.level.ServerLevel +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +abstract class RenderEntity(var world: Level?, var pos: Vec3 = Vec3.ZERO) : ServerControler, + Tickable { + var renderRange = 256.0 + + val client: Boolean + get() = world?.isClientSide ?: false + + var alwaysToggle = false + private var syncOnce = false + + companion object { + fun decodeBase(buf: PacketByteBuf, instance: RenderEntity) { + instance.uuid = buf.readUUID() + instance.pos = buf.readVec3() + instance.canceled = buf.readBoolean() + instance.age = buf.readInt() + instance.dirty = false + } + + fun encodeBase(buf: PacketByteBuf, entity: RenderEntity) { + buf.writeUUID(entity.uuid) + buf.writeVec3(entity.pos) + buf.writeBoolean(entity.canceled) + buf.writeInt(entity.age) + } + + fun createCodec( + factory: () -> T, + encodeExtra: (PacketByteBuf, T) -> Unit = { _, _ -> }, + decodeExtra: (PacketByteBuf, T) -> Unit = { _, _ -> } + ): ForgeStreamCodec { + return ForgeStreamCodec.of( + { buf, entity -> + encodeBase(buf, entity) + @Suppress("UNCHECKED_CAST") + val typed = entity as T + encodeExtra(buf, typed) + }, + { buf -> + val instance = factory() + decodeBase(buf, instance) + decodeExtra(buf, instance) + instance + } + ) + } + } + + var lastRenderPos = pos + internal set + + var age = 0 + + var uuid: UUID = UUID.randomUUID() + + var dirty = false + + var canceled = false + private val preTickActions = ArrayList Unit>() + private val postTickActions = ArrayList Unit>() + + override fun tick() { + if (canceled) return + age++ + val stableSize = preTickActions.size + var index = 0 + while (index < stableSize) { + preTickActions[index](this) + index++ + } + if (client) { + clientTick() + } else { + serverTick() + } + postTickActions.forEach { it(this) } + } + + final override fun addPreTickAction(action: RenderEntity.() -> Unit): Tickable { + preTickActions.add(action) + return this + } + + final override fun addPreTickActionPost(action: RenderEntity.() -> Unit): Tickable { + postTickActions.add(action) + return this + } + + open fun clientTick() { + } + + open fun serverTick() { + } + + fun getTogglePacket(): PacketRenderEntityS2C? { + return getTogglePacket(false) + } + + fun getTogglePacket(force: Boolean): PacketRenderEntityS2C? { + return getPacket(PacketRenderEntityS2C.Method.TOGGLE, force) + } + + fun getPacket(method: PacketRenderEntityS2C.Method): PacketRenderEntityS2C? { + return getPacket(method, false) + } + + fun getPacket(method: PacketRenderEntityS2C.Method, force: Boolean): PacketRenderEntityS2C? { + if (!force && !dirty && method == PacketRenderEntityS2C.Method.TOGGLE) { + return null + } + val buf = PacketByteBuf(Unpooled.buffer()) + getCodec().encode(buf, this) + val bytes = ByteArray(buf.readableBytes()) + buf.readBytes(bytes) + val packet = PacketRenderEntityS2C(uuid, bytes, getRenderID(), method) + return packet + } + + fun setPosition(pos: Vec3) { + this.pos = pos + markDirty() + } + + fun markDirty() { + dirty = true + } + + fun requestSync() { + dirty = true + syncOnce = true + } + + fun clearDirty() { + dirty = false + syncOnce = false + } + + internal fun onSynced() { + dirty = false + syncOnce = false + } + + open fun shouldSync(): Boolean { + return alwaysToggle || dirty + } + + protected fun tracked(initial: T, syncOnce: Boolean = false): ReadWriteProperty { + return object : ReadWriteProperty { + private var value = initial + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + if (this.value == value) return + this.value = value + if (syncOnce) { + requestSync() + } else { + markDirty() + } + } + } + } + + fun getTime(delta: Float): Float { + return (age + delta) / 20 + } + + open fun loadProfileFromEntity(another: RenderEntity) { + this.age = another.age + this.canceled = another.canceled + this.pos = another.pos + this.uuid = another.uuid + this.world = another.world + } + + abstract fun getCodec(): ForgeStreamCodec + + abstract fun getRenderID(): ResourceLocation + + override fun teleportTo(to: Vec3) { + this.lastRenderPos = this.pos + this.pos = to + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + override fun rotateToPoint(to: RelativeLocation) { + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + } + + override fun rotateAsAxis(radian: Double) { + } + + override fun remove() { + this.canceled = true + } + + override fun getValue(): RenderEntity { + return this + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ServerLevel) return + this.world = world + this.pos = pos + ServerRenderEntityManager.spawn(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/pipeline/CooUniformValue.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/pipeline/CooUniformValue.kt new file mode 100644 index 00000000..6026a18b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/pipeline/CooUniformValue.kt @@ -0,0 +1,263 @@ +package cn.coostack.cooparticlesapi.renderer.pipeline + +import net.minecraft.network.PacketByteBuf +import org.joml.Matrix2dc +import org.joml.Matrix2fc +import org.joml.Matrix3dc +import org.joml.Matrix3fc +import org.joml.Matrix3x2dc +import org.joml.Matrix3x2fc +import org.joml.Matrix4dc +import org.joml.Matrix4fc +import org.joml.Matrix4x3dc +import org.joml.Matrix4x3fc + +sealed interface CooUniformValue { + companion object { + private const val FLOAT = 0 + private const val INT = 1 + private const val VEC2 = 2 + private const val VEC3 = 3 + private const val VEC4 = 4 + private const val BOOL = 5 + private const val UINT = 6 + private const val DOUBLE = 7 + private const val IVEC = 8 + private const val UVEC = 9 + private const val BVEC = 10 + private const val DVEC = 11 + private const val MAT = 12 + private const val DMAT = 13 + private const val SAMPLER = 14 + private const val IMAGE = 15 + private const val ARRAY = 16 + + val STREAM_CODEC: ForgeStreamCodec = ForgeStreamCodec.of(::encode, ::decode) + + private fun encode(buffer: PacketByteBuf, value: CooUniformValue) { + when (value) { + is FloatValue -> { + buffer.writeByte(FLOAT) + buffer.writeFloat(value.value) + } + is IntValue -> { + buffer.writeByte(INT) + buffer.writeVarInt(zigZag(value.value)) + } + is Vec2Value -> { + buffer.writeByte(VEC2) + buffer.writeFloat(value.x) + buffer.writeFloat(value.y) + } + is Vec3Value -> { + buffer.writeByte(VEC3) + buffer.writeFloat(value.x) + buffer.writeFloat(value.y) + buffer.writeFloat(value.z) + } + is Vec4Value -> { + buffer.writeByte(VEC4) + buffer.writeFloat(value.x) + buffer.writeFloat(value.y) + buffer.writeFloat(value.z) + buffer.writeFloat(value.w) + } + is BoolValue -> { + buffer.writeByte(BOOL) + buffer.writeBoolean(value.value) + } + is UIntValue -> { + buffer.writeByte(UINT) + buffer.writeInt(value.value.toInt()) + } + is DoubleValue -> { + buffer.writeByte(DOUBLE) + buffer.writeDouble(value.value) + } + is IVecValue -> { + buffer.writeByte(IVEC) + buffer.writeByte(value.components.size) + value.components.forEach(buffer::writeInt) + } + is UVecValue -> { + buffer.writeByte(UVEC) + buffer.writeByte(value.components.size) + value.components.forEach { buffer.writeInt(it.toInt()) } + } + is BVecValue -> { + buffer.writeByte(BVEC) + buffer.writeByte(value.components.size) + value.components.forEach(buffer::writeBoolean) + } + is DVecValue -> { + buffer.writeByte(DVEC) + buffer.writeByte(value.components.size) + value.components.forEach(buffer::writeDouble) + } + is MatValue -> { + buffer.writeByte(MAT) + writeMatrixShape(buffer, value.columns, value.rows) + value.components.forEach(buffer::writeFloat) + } + is DMatValue -> { + buffer.writeByte(DMAT) + writeMatrixShape(buffer, value.columns, value.rows) + value.components.forEach(buffer::writeDouble) + } + is SamplerValue -> { + buffer.writeByte(SAMPLER) + buffer.writeInt(value.textureUnit) + } + is ImageValue -> { + buffer.writeByte(IMAGE) + buffer.writeInt(value.imageUnit) + } + is ArrayValue -> { + buffer.writeByte(ARRAY) + buffer.writeVarInt(value.elements.size) + value.elements.forEach { encode(buffer, it) } + } + } + } + + private fun decode(buffer: PacketByteBuf): CooUniformValue { + return when (val type = buffer.readUnsignedByte().toInt()) { + FLOAT -> FloatValue(buffer.readFloat()) + INT -> IntValue(unZigZag(buffer.readVarInt())) + VEC2 -> Vec2Value(buffer.readFloat(), buffer.readFloat()) + VEC3 -> Vec3Value(buffer.readFloat(), buffer.readFloat(), buffer.readFloat()) + VEC4 -> Vec4Value( + buffer.readFloat(), + buffer.readFloat(), + buffer.readFloat(), + buffer.readFloat() + ) + BOOL -> BoolValue(buffer.readBoolean()) + UINT -> UIntValue(buffer.readInt().toUInt()) + DOUBLE -> DoubleValue(buffer.readDouble()) + IVEC -> IVecValue(readList(buffer) { readInt() }) + UVEC -> UVecValue(readList(buffer) { readInt().toUInt() }) + BVEC -> BVecValue(readList(buffer) { readBoolean() }) + DVEC -> DVecValue(readList(buffer) { readDouble() }) + MAT -> { + val (columns, rows) = readMatrixShape(buffer) + MatValue(columns, rows, List(columns * rows) { buffer.readFloat() }) + } + DMAT -> { + val (columns, rows) = readMatrixShape(buffer) + DMatValue(columns, rows, List(columns * rows) { buffer.readDouble() }) + } + SAMPLER -> SamplerValue(buffer.readInt()) + IMAGE -> ImageValue(buffer.readInt()) + ARRAY -> ArrayValue(List(buffer.readVarInt()) { decode(buffer) }) + else -> error("Unknown uniform value type: $type") + } + } + + private fun writeMatrixShape(buffer: PacketByteBuf, columns: Int, rows: Int) { + buffer.writeByte(columns) + buffer.writeByte(rows) + } + + private fun readMatrixShape(buffer: PacketByteBuf): Pair { + return buffer.readUnsignedByte().toInt() to buffer.readUnsignedByte().toInt() + } + + private fun readList(buffer: PacketByteBuf, readElement: PacketByteBuf.() -> T): List { + return List(buffer.readUnsignedByte().toInt()) { buffer.readElement() } + } + + private fun zigZag(value: Int): Int = (value shl 1) xor (value shr 31) + private fun unZigZag(value: Int): Int = (value ushr 1) xor -(value and 1) + } + + data class BoolValue(val value: Boolean) : CooUniformValue + data class IntValue(val value: Int) : CooUniformValue + data class UIntValue(val value: UInt) : CooUniformValue + data class FloatValue(val value: Float) : CooUniformValue + data class DoubleValue(val value: Double) : CooUniformValue + data class Vec2Value(val x: Float, val y: Float) : CooUniformValue + data class Vec3Value(val x: Float, val y: Float, val z: Float) : CooUniformValue + data class Vec4Value(val x: Float, val y: Float, val z: Float, val w: Float) : CooUniformValue + + class IVecValue(components: List) : CooUniformValue { + val components: List = immutableList(components) + constructor(vararg components: Int) : this(components.toList()) + init { requireVectorSize(this.components.size) } + } + + class UVecValue(components: List) : CooUniformValue { + val components: List = immutableList(components) + constructor(x: UInt, y: UInt) : this(listOf(x, y)) + constructor(x: UInt, y: UInt, z: UInt) : this(listOf(x, y, z)) + constructor(x: UInt, y: UInt, z: UInt, w: UInt) : this(listOf(x, y, z, w)) + init { requireVectorSize(this.components.size) } + } + + class BVecValue(components: List) : CooUniformValue { + val components: List = immutableList(components) + constructor(vararg components: Boolean) : this(components.toList()) + init { requireVectorSize(this.components.size) } + } + + class DVecValue(components: List) : CooUniformValue { + val components: List = immutableList(components) + constructor(vararg components: Double) : this(components.toList()) + init { requireVectorSize(this.components.size) } + } + + class MatValue( + val columns: Int, + val rows: Int, + components: List + ) : CooUniformValue { + val components: List = immutableList(components) + constructor(columns: Int, rows: Int, vararg components: Float) : this(columns, rows, components.toList()) + constructor(value: Matrix2fc) : this(2, 2, value.get(FloatArray(4)).toList()) + constructor(value: Matrix3x2fc) : this(3, 2, value.get(FloatArray(6)).toList()) + constructor(value: Matrix3fc) : this(3, 3, value.get(FloatArray(9)).toList()) + constructor(value: Matrix4x3fc) : this(4, 3, value.get(FloatArray(12)).toList()) + constructor(value: Matrix4fc) : this(4, 4, value.get(FloatArray(16)).toList()) + init { requireMatrixShape(columns, rows, this.components.size) } + } + + class DMatValue( + val columns: Int, + val rows: Int, + components: List + ) : CooUniformValue { + val components: List = immutableList(components) + constructor(columns: Int, rows: Int, vararg components: Double) : this(columns, rows, components.toList()) + constructor(value: Matrix2dc) : this(2, 2, value.get(DoubleArray(4)).toList()) + constructor(value: Matrix3x2dc) : this(3, 2, value.get(DoubleArray(6)).toList()) + constructor(value: Matrix3dc) : this(3, 3, value.get(DoubleArray(9)).toList()) + constructor(value: Matrix4x3dc) : this(4, 3, value.get(DoubleArray(12)).toList()) + constructor(value: Matrix4dc) : this(4, 4, value.get(DoubleArray(16)).toList()) + init { requireMatrixShape(columns, rows, this.components.size) } + } + + data class SamplerValue(val textureUnit: Int) : CooUniformValue + data class ImageValue(val imageUnit: Int) : CooUniformValue + + class ArrayValue(elements: List) : CooUniformValue { + val elements: List = immutableList(elements) + constructor(vararg elements: CooUniformValue) : this(elements.toList()) + init { + require(this.elements.isNotEmpty()) { "Uniform array cannot be empty" } + require(this.elements.none { it is ArrayValue }) { "Nested uniform arrays are not supported by GLSL" } + } + } +} + +private fun immutableList(values: Collection): List = java.util.List.copyOf(values) +private fun requireVectorSize(size: Int) { + require(size in 2..4) { "GLSL vector size must be between 2 and 4: $size" } +} +private fun requireMatrixShape(columns: Int, rows: Int, componentCount: Int) { + require(columns in 2..4 && rows in 2..4) { + "GLSL matrix dimensions must be between 2 and 4: ${columns}x$rows" + } + require(componentCount == columns * rows) { + "GLSL ${columns}x$rows matrix requires ${columns * rows} components: $componentCount" + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectBinding.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectBinding.kt new file mode 100644 index 00000000..e189e761 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectBinding.kt @@ -0,0 +1,291 @@ +package cn.coostack.cooparticlesapi.renderer.post + +import net.minecraft.core.BlockPos +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +/** + * 描述一个 post effect 实例“绑定到哪里”。 + * + * 绑定不是输入 texture,而是效果的空间语义。OpenGL backend 会根据 binding 计算内置 uniform: + * + * - `center`:屏幕归一化坐标,供 shockwave / halo / distortion 这类局部效果使用 + * - `sourceDepth`:绑定点投影后的深度,供深度裁剪或遮挡逻辑使用 + * + * 这个类型替代了调用方在每个 shader 里重复实现“实体/方块/世界坐标投影到屏幕坐标”的样板。 + */ +internal sealed interface PostEffectBinding { + /** + * 写入绑定类型对应的数据字段。 + * + * 该方法只写实现自身的数据,不写类型 id;需要跨网络传输时应调用 [writeTyped]。 + * 读端必须使用相同的字段顺序,否则后续包字段会发生错位。 + * + * @param buf 目标网络缓冲区 + */ + fun write(buf: PacketByteBuf) + + /** 绑定整屏效果,`center` 默认为屏幕中心。 */ + data object Screen : PostEffectBinding { + /** + * 按 `Screen` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) = Unit + } + + /** + * 绑定屏幕归一化坐标,x/y 通常位于 0..1。 + * + * @property x 水平屏幕坐标,0 表示左侧,1 表示右侧 + * @property y 垂直屏幕坐标,0 表示顶部,1 表示底部 + * 示例:`PostEffectBinding.ScreenPoint(0.5F, 0.5F)` 绑定屏幕中心。 + */ + data class ScreenPoint(val x: Float, val y: Float) : PostEffectBinding { + /** + * 按 `ScreenPoint` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeFloat(x) + buf.writeFloat(y) + } + } + + /** + * 绑定世界坐标。`level == null` 时表示客户端当前世界。 + * + * @property level 维度资源位置;为空时使用当前客户端世界 + * @property x 世界 X 坐标 + * @property y 世界 Y 坐标 + * @property z 世界 Z 坐标 + */ + data class WorldPos(val level: ResourceLocation?, val x: Double, val y: Double, val z: Double) : PostEffectBinding { + /** + * 按 `WorldPos` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeBoolean(level != null) + level?.let(buf::writeResourceLocation) + buf.writeDouble(x) + buf.writeDouble(y) + buf.writeDouble(z) + } + } + + /** + * 绑定实体 id,客户端会在当前 level 中查找实体并投影其中心位置。 + * + * @property entityId 当前世界中实体的运行时 id + * 示例:`PostEffectBinding.Entity(entity.id)`。 + */ + data class Entity(val entityId: Int) : PostEffectBinding { + /** + * 按 `Entity` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeInt(entityId) + } + } + + /** + * 绑定玩家 UUID,适合跨维度/多人场景中明确指定玩家。 + * + * @property playerId 玩家持久 UUID;客户端按 UUID 查找在线玩家 + */ + data class Player(val playerId: UUID) : PostEffectBinding { + /** + * 按 `Player` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeUUID(playerId) + } + } + + /** + * 绑定方块位置。 + * + * [offset] 是方块内偏移,默认使用方块中心 `(0.5, 0.5, 0.5)`。 + * 复杂例子:绑定到方块顶面中心可传 `Vec3Value(0.5, 1.0, 0.5)`。 + * + * @property level 维度资源位置;为空时使用客户端当前世界 + * @property pos 方块的整数坐标 + * @property offset 方块内偏移;为空时由 backend 使用方块中心 + */ + data class Block(val level: ResourceLocation?, val pos: BlockPos, val offset: PostEffectParamValue.Vec3Value? = null) : + PostEffectBinding { + /** + * 按 `Block` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeBoolean(level != null) + level?.let(buf::writeResourceLocation) + buf.writeBlockPos(pos) + buf.writeBoolean(offset != null) + offset?.write(buf) + } + } + + /** + * 绑定物品语义。当前默认投影到屏幕中心,保留给 item GUI / hand / world item 扩展。 + * + * @property itemId 物品资源位置;为空时只保留 context 语义 + * @property context 物品所在的渲染上下文,会影响未来 backend 的投影选择 + */ + data class Item(val itemId: ResourceLocation?, val context: PostEffectItemContext) : PostEffectBinding { + /** + * 按 `Item` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeBoolean(itemId != null) + itemId?.let(buf::writeResourceLocation) + buf.writeUtf(context.name) + } + } + + /** + * 自定义绑定。 + * + * 用于框架未覆盖的业务定位方式。payload 会被同步,但默认 OpenGL backend 不理解业务含义; + * 需要自定义 descriptor/executor 或在 shader 参数中额外传递所需坐标。 + * + * @property key 自定义绑定类型的资源位置,读写双方必须约定其含义 + * @property payload 类型专用的二进制数据,默认为空数组 + */ + data class Custom(val key: ResourceLocation, val payload: ByteArray = ByteArray(0)) : PostEffectBinding { + /** + * 按 `Custom` 约定的字段顺序写入 `write` 数据;读取端必须使用相同协议。 + * + * 示例:`write(buf = buf)`。 + * + * @param buf 承载本次读写数据的缓冲区,调用前必须位于约定字段起点 + */ + override fun write(buf: PacketByteBuf) { + buf.writeResourceLocation(key) + buf.writeByteArray(payload) + } + + /** + * 执行 `Custom` 定义的 `equals` 操作;输入和返回值用于该组件当前的渲染职责。 + * + * 示例:`equals(other = other)`。 + * + * @param other 当前操作需要的输入值;其语义由方法名和所属组件共同限定 + * + * @return 当前操作计算、更新或查询得到的结果 + */ + override fun equals(other: Any?): Boolean { + return other is Custom && key == other.key && payload.contentEquals(other.payload) + } + + /** + * 执行 `Custom` 定义的 `hashCode` 操作;输入和返回值用于该组件当前的渲染职责。 + * + * 示例:`hashCode()`。 + * + * @return 当前操作计算、更新或查询得到的结果 + */ + override fun hashCode(): Int = 31 * key.hashCode() + payload.contentHashCode() + } + + companion object { + /** + * 写入绑定类型 id 和绑定数据。 + * + * 示例:`PostEffectBinding.writeTyped(buf, PostEffectBinding.Screen)`。 + * + * @param buf 目标网络缓冲区 + * @param binding 要序列化的绑定实例 + */ + fun writeTyped(buf: PacketByteBuf, binding: PostEffectBinding) { + buf.writeUtf(binding.typeId) + binding.write(buf) + } + + /** + * 从网络缓冲区读取绑定类型 id 并构造对应实现。 + * + * @param buf 已定位到绑定类型 id 的网络缓冲区 + * @return 解码后的绑定实例 + * @throws IllegalStateException 类型 id 未注册时抛出 + */ + fun readTyped(buf: PacketByteBuf): PostEffectBinding { + return when (val type = buf.readUtf()) { + "screen" -> Screen + "screen_point" -> ScreenPoint(buf.readFloat(), buf.readFloat()) + "world" -> WorldPos(readNullableId(buf), buf.readDouble(), buf.readDouble(), buf.readDouble()) + "entity" -> Entity(buf.readInt()) + "player" -> Player(buf.readUUID()) + "block" -> { + val level = readNullableId(buf) + val pos = buf.readBlockPos() + val offset = if (buf.readBoolean()) { + PostEffectParamValue.Vec3Value(buf.readDouble(), buf.readDouble(), buf.readDouble()) + } else { + null + } + Block(level, pos, offset) + } + + "item" -> Item(readNullableId(buf), PostEffectItemContext.valueOf(buf.readUtf())) + "custom" -> Custom(buf.readResourceLocation(), buf.readByteArray()) + else -> error("Unknown post effect binding type: $type") + } + } + + private fun readNullableId(buf: PacketByteBuf): ResourceLocation? { + return if (buf.readBoolean()) buf.readResourceLocation() else null + } + } +} + +internal val PostEffectBinding.typeId: String + get() = when (this) { + is PostEffectBinding.Screen -> "screen" + is PostEffectBinding.ScreenPoint -> "screen_point" + is PostEffectBinding.WorldPos -> "world" + is PostEffectBinding.Entity -> "entity" + is PostEffectBinding.Player -> "player" + is PostEffectBinding.Block -> "block" + is PostEffectBinding.Item -> "item" + is PostEffectBinding.Custom -> "custom" + } + +/** 物品绑定的语义位置。当前默认 backend 只保留语义,后续可扩展为不同投影方式。 */ +internal enum class PostEffectItemContext { + /** 物品在 GUI 中渲染,例如背包或 JEI 类界面。 */ + GUI, + /** 第一人称手持物品。 */ + FIRST_PERSON_HAND, + /** 第三人称手持物品。 */ + THIRD_PERSON_HAND, + /** 掉落物或物品实体。 */ + WORLD_ENTITY +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectLifecycle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectLifecycle.kt new file mode 100644 index 00000000..2a7b6cc9 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectLifecycle.kt @@ -0,0 +1,113 @@ +package cn.coostack.cooparticlesapi.renderer.post + +import net.minecraft.network.PacketByteBuf +import kotlin.math.max +import kotlin.math.min + +/** + * post effect 实例的生命周期。 + * + * [durationTicks] 决定实例何时过期,[progress] 是 `ageTicks / durationTicks` 的 0..1 值, + * 会作为内置 uniform `progress` 上传给 shader。 + * + * `warmup/expand/hold/fade` 用于描述阶段,而不是自动改变输出。shader 可以读取 `progress` + * 或业务方通过参数决定不同阶段的行为。当前阶段可通过 [phase] 查询。 + * + * 复杂例子: + * + * ```kotlin + * PostEffectLifecycle( + * durationTicks = 80, + * warmupTicks = 5, + * expandTicks = 25, + * holdTicks = 30, + * fadeTicks = 20 + * ) + * ``` + * + * 这个类替代调用方手动维护 age、过期判断、progress 计算和网络同步字段。 + */ +internal data class PostEffectLifecycle( + val durationTicks: Int, + val ageTicks: Int = 0, + val warmupTicks: Int = 0, + val expandTicks: Int = durationTicks, + val holdTicks: Int = 0, + val fadeTicks: Int = 0 +) { + /** 当前生命周期进度,范围 0..1。duration <= 0 时固定为 1。 */ + val progress: Float + get() = if (durationTicks <= 0) 1f else (ageTicks.toFloat() / durationTicks.toFloat()).coerceIn(0f, 1f) + + /** 当前阶段,用于业务逻辑或自定义 executor 判断。 */ + val phase: PostEffectLifecyclePhase + get() { + var cursor = max(0, warmupTicks) + if (ageTicks < cursor) return PostEffectLifecyclePhase.WARMUP + cursor += max(0, expandTicks) + if (ageTicks < cursor) return PostEffectLifecyclePhase.EXPAND + cursor += max(0, holdTicks) + if (ageTicks < cursor) return PostEffectLifecyclePhase.HOLD + cursor += max(0, fadeTicks) + if (ageTicks < cursor) return PostEffectLifecyclePhase.FADE + return PostEffectLifecyclePhase.DONE + } + + /** 是否已达到总时长。durationTicks < 0 可表达“不按 duration 自动过期”。 */ + val expired: Boolean + get() = durationTicks >= 0 && ageTicks >= durationTicks + + /** + * 推进一个游戏 tick,返回新的不可变生命周期快照。 + * + * @return age 增加 1 的生命周期;已接近整数上限时保持在安全范围 + */ + fun tick(): PostEffectLifecycle = copy(ageTicks = min(Int.MAX_VALUE - 1, ageTicks + 1)) + + /** + * 按固定字段顺序写入生命周期状态,供服务端和客户端同步。 + * + * @param buf 目标网络缓冲区 + */ + fun write(buf: PacketByteBuf) { + buf.writeInt(durationTicks) + buf.writeInt(ageTicks) + buf.writeInt(warmupTicks) + buf.writeInt(expandTicks) + buf.writeInt(holdTicks) + buf.writeInt(fadeTicks) + } + + companion object { + /** + * 读取 [write] 写出的生命周期字段。 + * + * @param buf 已定位到 durationTicks 字段的网络缓冲区 + * @return 解码后的生命周期快照 + */ + fun read(buf: PacketByteBuf): PostEffectLifecycle { + return PostEffectLifecycle( + durationTicks = buf.readInt(), + ageTicks = buf.readInt(), + warmupTicks = buf.readInt(), + expandTicks = buf.readInt(), + holdTicks = buf.readInt(), + fadeTicks = buf.readInt() + ) + } + } +} + +/** 生命周期阶段。阶段只表达语义,不会自动改变 shader;需要 shader 或业务代码读取后自行使用。 */ +internal enum class PostEffectLifecyclePhase { + /** 预热期,适合做淡入、预采样或延迟触发。 */ + WARMUP, + /** 展开期,适合半径、强度、mask 范围从小到大变化。 */ + EXPAND, + /** 保持期,适合稳定显示。 */ + HOLD, + /** 消退期,适合透明度或强度降低。 */ + FADE, + /** 已完成,通常实例会被清理或不再提交。 */ + DONE +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt new file mode 100644 index 00000000..7fc37ca6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt @@ -0,0 +1,194 @@ +package cn.coostack.cooparticlesapi.renderer.post + +import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +internal sealed interface PostEffectParamValue { + fun write(buf: PacketByteBuf) + + data class BoolValue(val value: Boolean) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeBoolean(value) + } + } + + data class IntValue(val value: Int) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeInt(value) + } + } + + data class LongValue(val value: Long) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeLong(value) + } + } + + data class FloatValue(val value: Float) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeFloat(value) + } + } + + data class DoubleValue(val value: Double) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeDouble(value) + } + } + + data class StringValue(val value: String) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeUtf(value) + } + } + + data class ResourceValue(val value: ResourceLocation) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeResourceLocation(value) + } + } + + data class Vec2Value(val x: Float, val y: Float) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeFloat(x) + buf.writeFloat(y) + } + } + + data class Vec3Value(val x: Double, val y: Double, val z: Double) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeDouble(x) + buf.writeDouble(y) + buf.writeDouble(z) + } + } + + data class ColorValue(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + buf.writeFloat(red) + buf.writeFloat(green) + buf.writeFloat(blue) + buf.writeFloat(alpha) + } + } + + data class UniformValue(val value: CooUniformValue) : PostEffectParamValue { + override fun write(buf: PacketByteBuf) { + CooUniformValue.STREAM_CODEC.encode(buf, value) + } + } + + companion object { + fun writeTyped(buf: PacketByteBuf, value: PostEffectParamValue) { + buf.writeUtf(value.typeId) + value.write(buf) + } + + fun readTyped(buf: PacketByteBuf): PostEffectParamValue { + return when (val type = buf.readUtf()) { + "bool" -> BoolValue(buf.readBoolean()) + "int" -> IntValue(buf.readInt()) + "long" -> LongValue(buf.readLong()) + "float" -> FloatValue(buf.readFloat()) + "double" -> DoubleValue(buf.readDouble()) + "string" -> StringValue(buf.readUtf()) + "resource" -> ResourceValue(buf.readResourceLocation()) + "vec2" -> Vec2Value(buf.readFloat(), buf.readFloat()) + "vec3" -> Vec3Value(buf.readDouble(), buf.readDouble(), buf.readDouble()) + "color" -> ColorValue(buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readFloat()) + "uniform" -> UniformValue(CooUniformValue.STREAM_CODEC.decode(buf)) + else -> error("Unknown post effect param type: $type") + } + } + } +} + +internal val PostEffectParamValue.typeId: String + get() = when (this) { + is PostEffectParamValue.BoolValue -> "bool" + is PostEffectParamValue.IntValue -> "int" + is PostEffectParamValue.LongValue -> "long" + is PostEffectParamValue.FloatValue -> "float" + is PostEffectParamValue.DoubleValue -> "double" + is PostEffectParamValue.StringValue -> "string" + is PostEffectParamValue.ResourceValue -> "resource" + is PostEffectParamValue.Vec2Value -> "vec2" + is PostEffectParamValue.Vec3Value -> "vec3" + is PostEffectParamValue.ColorValue -> "color" + is PostEffectParamValue.UniformValue -> "uniform" + } + +internal fun CooUniformValue.toPostEffectParamValue(): PostEffectParamValue { + return when (this) { + is CooUniformValue.BoolValue -> PostEffectParamValue.BoolValue(value) + is CooUniformValue.IntValue -> PostEffectParamValue.IntValue(value) + is CooUniformValue.FloatValue -> PostEffectParamValue.FloatValue(value) + is CooUniformValue.Vec2Value -> PostEffectParamValue.Vec2Value(x, y) + is CooUniformValue.Vec3Value -> PostEffectParamValue.Vec3Value(x.toDouble(), y.toDouble(), z.toDouble()) + is CooUniformValue.Vec4Value -> PostEffectParamValue.ColorValue(x, y, z, w) + is CooUniformValue.UIntValue, + is CooUniformValue.DoubleValue, + is CooUniformValue.IVecValue, + is CooUniformValue.UVecValue, + is CooUniformValue.BVecValue, + is CooUniformValue.DVecValue, + is CooUniformValue.MatValue, + is CooUniformValue.DMatValue, + is CooUniformValue.SamplerValue, + is CooUniformValue.ImageValue, + is CooUniformValue.ArrayValue -> PostEffectParamValue.UniformValue(this) + } +} + +internal data class PostEffectParams( + private val values: Map = emptyMap() +) { + fun asMap(): Map = values + + operator fun get(name: String): PostEffectParamValue? = values[name] + + fun plus(name: String, value: PostEffectParamValue): PostEffectParams = PostEffectParams(values + (name to value)) + + fun write(buf: PacketByteBuf) { + buf.writeInt(values.size) + values.toSortedMap().forEach { (name, value) -> + buf.writeUtf(name) + PostEffectParamValue.writeTyped(buf, value) + } + } + + companion object { + val EMPTY = PostEffectParams() + + fun read(buf: PacketByteBuf): PostEffectParams { + val count = buf.readInt() + val values = LinkedHashMap(count) + repeat(count) { + values[buf.readUtf()] = PostEffectParamValue.readTyped(buf) + } + return PostEffectParams(values) + } + } +} + +internal class PostEffectParamsBuilder { + private val values = LinkedHashMap() + + fun bool(name: String, value: Boolean) = apply { values[name] = PostEffectParamValue.BoolValue(value) } + fun int(name: String, value: Int) = apply { values[name] = PostEffectParamValue.IntValue(value) } + fun long(name: String, value: Long) = apply { values[name] = PostEffectParamValue.LongValue(value) } + fun float(name: String, value: Float) = apply { values[name] = PostEffectParamValue.FloatValue(value) } + fun double(name: String, value: Double) = apply { values[name] = PostEffectParamValue.DoubleValue(value) } + fun string(name: String, value: String) = apply { values[name] = PostEffectParamValue.StringValue(value) } + fun resource(name: String, value: ResourceLocation) = apply { values[name] = PostEffectParamValue.ResourceValue(value) } + fun vec2(name: String, x: Float, y: Float) = apply { values[name] = PostEffectParamValue.Vec2Value(x, y) } + fun vec3(name: String, x: Double, y: Double, z: Double) = apply { values[name] = PostEffectParamValue.Vec3Value(x, y, z) } + fun color(name: String, red: Float, green: Float, blue: Float, alpha: Float = 1F) = apply { + values[name] = PostEffectParamValue.ColorValue(red, green, blue, alpha) + } + + fun put(name: String, value: PostEffectParamValue) = apply { values[name] = value } + + fun build(): PostEffectParams = PostEffectParams(values.toMap()) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityRegistry.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityRegistry.kt new file mode 100644 index 00000000..e4db5235 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityRegistry.kt @@ -0,0 +1,82 @@ +package cn.coostack.cooparticlesapi.renderer.runtime + +import cn.coostack.cooparticlesapi.renderer.RenderEntity +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +object ClientRenderEntityRegistry { + private val types = LinkedHashMap() + private val automaticEntityClasses = LinkedHashMap>() + private val renderers = LinkedHashMap>() + + @Synchronized + fun register(id: ResourceLocation, type: ClientRenderEntityType) { + if (types.containsKey(id)) { + throw IllegalArgumentException(id.toString()) + } + types[id] = type + } + + fun register( + id: ResourceLocation, + codec: ForgeStreamCodec, + rendererFactory: (() -> RenderEntityRenderer)? = null + ) { + register(id, ClientRenderEntityType(codec, rendererFactory)) + } + + @Synchronized + fun registerRenderer(id: ResourceLocation, rendererFactory: () -> RenderEntityRenderer) { + val existing = types[id] + ?: throw IllegalStateException("RenderEntity codec not registered: $id") + types[id] = existing.copy(rendererFactory = rendererFactory) + renderers.remove(id) + } + + @Synchronized + internal fun applyRegistrations(registrations: Map) { + registrations.forEach { (id, registration) -> + val existing = types[id] + val existingEntityClass = automaticEntityClasses[id] + check(existing == null || existingEntityClass == registration.entityClass) { + "RenderEntity id ownership changed during automatic registration: $id" + } + } + registrations.forEach { (id, registration) -> + types[id] = registration.type + automaticEntityClasses[id] = registration.entityClass + renderers.remove(id) + } + } + + @Synchronized + internal fun getAutomaticEntityClass(id: ResourceLocation): Class? { + return automaticEntityClasses[id] + } + + @Synchronized + fun get(id: ResourceLocation): ClientRenderEntityType? { + return types[id] + } + + @Synchronized + fun resolveRenderer(id: ResourceLocation): RenderEntityRenderer? { + renderers[id]?.let { return it } + val factory = types[id]?.rendererFactory ?: return null + return factory().also { renderer -> + renderers[id] = renderer + } + } + + @Synchronized + fun clear() { + types.clear() + automaticEntityClasses.clear() + renderers.clear() + } +} + +internal data class AutomaticClientRenderEntityType( + val type: ClientRenderEntityType, + val entityClass: Class +) diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityType.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityType.kt new file mode 100644 index 00000000..9c627376 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityType.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.renderer.runtime + +import cn.coostack.cooparticlesapi.renderer.RenderEntity +import net.minecraft.network.PacketByteBuf + +data class ClientRenderEntityType( + val codec: ForgeStreamCodec, + val rendererFactory: (() -> RenderEntityRenderer)? = null +) diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/RenderEntityAutoRegistry.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/RenderEntityAutoRegistry.kt new file mode 100644 index 00000000..12ed400b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/RenderEntityAutoRegistry.kt @@ -0,0 +1,302 @@ +package cn.coostack.cooparticlesapi.renderer.runtime + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.annotations.CooAutoRegisterRenderer +import cn.coostack.cooparticlesapi.coofx.server.CooFxSceneRenderEntity +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo +import cn.coostack.cooparticlesapi.renderer.RenderEntity +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.lang.reflect.Constructor +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Modifier +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.TypeVariable + +object RenderEntityAutoRegistry { + private var scannerRegistered = false + + @Synchronized + fun registerScanner() { + if (scannerRegistered) return + CooParticlesConstants.logger.info("正在自动注册 RenderEntity") + val entityClasses = CooAPIScanner.getWithAnnotation(CooAutoRegister::class.java) + .map { candidate -> loadClass(candidate, "RenderEntity") } + val rendererClasses = CooAPIScanner.getWithAnnotation(CooAutoRegisterRenderer::class.java) + .map { candidate -> loadClass(candidate, "RenderEntity renderer") } + registerClasses(entityClasses, rendererClasses) + scannerRegistered = true + } + + internal fun registerClasses( + entityClasses: Collection>, + rendererClasses: Collection> + ) { + val errors = ArrayList() + val failures = ArrayList() + val entities = entityClasses + .asSequence() + .filter { clazz -> RenderEntity::class.java.isAssignableFrom(clazz) } + .sortedBy { clazz -> clazz.name } + .mapNotNull { clazz -> + try { + describeEntity(clazz) + } catch (error: Exception) { + failures += descriptorFailure("RenderEntity", clazz, error) + null + } + } + .toList() + + entities.groupBy { descriptor -> descriptor.id } + .filterValues { descriptors -> descriptors.size > 1 } + .forEach { (id, descriptors) -> + errors += "Duplicate RenderEntity id $id: ${descriptors.joinToString { it.entityClass.name }}" + } + + val renderers = rendererClasses + .sortedBy { clazz -> clazz.name } + .mapNotNull { clazz -> + try { + describeRenderer(clazz) + } catch (error: Exception) { + failures += descriptorFailure("RenderEntity renderer", clazz, error) + null + } + } + + renderers.groupBy { descriptor -> descriptor.entityClass } + .filterValues { descriptors -> descriptors.size > 1 } + .forEach { (entityClass, descriptors) -> + errors += "Duplicate RenderEntity renderer binding for ${entityClass.name}: " + + descriptors.joinToString { it.rendererClass.name } + } + + val entitiesByClass = entities.associateBy { descriptor -> descriptor.entityClass } + renderers.forEach { renderer -> + if (renderer.entityClass !in entitiesByClass) { + errors += "RenderEntity codec not discovered for renderer ${renderer.rendererClass.name}: " + + renderer.entityClass.name + } + } + throwIfInvalid(errors, failures) + + val renderersByEntity = renderers.associateBy { descriptor -> descriptor.entityClass } + val registrations = LinkedHashMap() + entities.sortedBy { descriptor -> descriptor.id.toString() }.forEach { entity -> + val rendererFactory = renderersByEntity[entity.entityClass]?.toFactory(entity.id) + val existing = ClientRenderEntityRegistry.get(entity.id) + val existingEntityClass = ClientRenderEntityRegistry.getAutomaticEntityClass(entity.id) + when { + existing == null -> { + registrations[entity.id] = AutomaticClientRenderEntityType( + ClientRenderEntityType(entity.codec, rendererFactory), + entity.entityClass + ) + } + existingEntityClass == null -> { + errors += "RenderEntity id already registered by an unknown entity type: ${entity.id}" + } + existingEntityClass != entity.entityClass -> { + errors += "RenderEntity id ${entity.id} belongs to ${existingEntityClass.name}, " + + "cannot bind ${entity.entityClass.name}" + } + existing.rendererFactory == null && rendererFactory != null -> { + registrations[entity.id] = AutomaticClientRenderEntityType( + existing.copy(rendererFactory = rendererFactory), + entity.entityClass + ) + } + } + } + throwIfInvalid(errors, failures) + ClientRenderEntityRegistry.applyRegistrations(registrations) + val sceneType = ClientRenderEntityRegistry.get(CooFxSceneRenderEntity.ID) + CooParticlesConstants.logger.info( + "[CooFX-REGISTRY] RenderEntity auto registration complete: total=${registrations.size}, " + + "sceneId=${CooFxSceneRenderEntity.ID}, sceneRegistered=${sceneType != null}, " + + "sceneRenderer=${sceneType?.rendererFactory != null}", + ) + } + + internal fun registerClass(clazz: Class<*>) { + if (!RenderEntity::class.java.isAssignableFrom(clazz)) return + val entity = describeEntity(clazz) + if (ClientRenderEntityRegistry.get(entity.id) == null) { + ClientRenderEntityRegistry.applyRegistrations( + mapOf( + entity.id to AutomaticClientRenderEntityType( + ClientRenderEntityType(entity.codec), + entity.entityClass + ) + ) + ) + } + } + + private fun loadClass(candidate: SimpleClassInfo, kind: String): Class<*> { + try { + return candidate.toClass(false) + } catch (error: Exception) { + throw IllegalStateException("Failed to load $kind class ${candidate.type}", error) + } + } + + private fun describeEntity(clazz: Class<*>): EntityDescriptor { + @Suppress("UNCHECKED_CAST") + val entityClass = clazz as Class + val instance = createInstance(entityClass) + return EntityDescriptor(entityClass, instance.getRenderID(), instance.getCodec()) + } + + private fun describeRenderer(clazz: Class<*>): RendererDescriptor { + if (!RenderEntityRenderer::class.java.isAssignableFrom(clazz)) { + throw IllegalStateException( + "Auto renderer must implement RenderEntityRenderer: ${clazz.name}" + ) + } + if (clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) { + throw IllegalStateException("Auto renderer must be a concrete class: ${clazz.name}") + } + val constructor = try { + clazz.getConstructor() + } catch (_: NoSuchMethodException) { + throw IllegalStateException("Auto renderer requires a public no-arg constructor: ${clazz.name}") + } + val entityClass = resolveRendererEntityClass(clazz) + ?: throw IllegalStateException("Cannot resolve RenderEntity type for auto renderer: ${clazz.name}") + return RendererDescriptor(entityClass, clazz, constructor) + } + + private fun resolveRendererEntityClass(rendererClass: Class<*>): Class? { + val entityType = findRendererEntityType(rendererClass, emptyMap()) ?: return null + val resolvedClass = rawClass(resolveType(entityType, emptyMap())) ?: return null + if (!RenderEntity::class.java.isAssignableFrom(resolvedClass)) return null + @Suppress("UNCHECKED_CAST") + return resolvedClass as Class + } + + private fun findRendererEntityType( + type: Type, + inheritedBindings: Map, Type> + ): Type? { + val rawClass: Class<*> + val bindings = LinkedHashMap(inheritedBindings) + when (type) { + is Class<*> -> rawClass = type + is ParameterizedType -> { + rawClass = type.rawType as? Class<*> ?: return null + rawClass.typeParameters.zip(type.actualTypeArguments).forEach { (variable, argument) -> + bindings[variable] = resolveType(argument, inheritedBindings) + } + } + else -> return null + } + + if (rawClass == RenderEntityRenderer::class.java) { + val parameter = rawClass.typeParameters.single() + return resolveType(bindings[parameter] ?: return null, bindings) + } + + rawClass.genericInterfaces.forEach { parent -> + findRendererEntityType(parent, bindings)?.let { return it } + } + val parent = rawClass.genericSuperclass ?: return null + return findRendererEntityType(parent, bindings) + } + + private fun resolveType(type: Type, bindings: Map, Type>): Type { + var resolved = type + val visited = HashSet>() + while (resolved is TypeVariable<*> && visited.add(resolved)) { + resolved = bindings[resolved] ?: return resolved + } + return resolved + } + + private fun rawClass(type: Type): Class<*>? { + return when (type) { + is Class<*> -> type + is ParameterizedType -> type.rawType as? Class<*> + else -> null + } + } + + private fun createInstance(type: Class): RenderEntity { + val noArgCtor = try { + type.getConstructor() + } catch (_: NoSuchMethodException) { + null + } + if (noArgCtor != null) return newEntityInstance(type, noArgCtor) + val levelVecCtor = try { + type.getConstructor(Level::class.java, Vec3::class.java) + } catch (_: NoSuchMethodException) { + throw IllegalStateException( + "RenderEntity requires public no-arg or (Level, Vec3) constructor: ${type.name}" + ) + } + return newEntityInstance(type, levelVecCtor, null, Vec3.ZERO) + } + + private fun newEntityInstance( + type: Class, + constructor: Constructor, + vararg arguments: Any? + ): RenderEntity { + try { + return constructor.newInstance(*arguments) + } catch (error: InvocationTargetException) { + throw IllegalStateException("Failed to create RenderEntity ${type.name}", error.targetException) + } catch (error: ReflectiveOperationException) { + throw IllegalStateException("Failed to create RenderEntity ${type.name}", error) + } + } + + private fun descriptorFailure(kind: String, clazz: Class<*>, error: Exception): IllegalStateException { + val detail = error.message?.takeIf { message -> message.isNotBlank() } ?: error.javaClass.name + return IllegalStateException("Failed to inspect $kind ${clazz.name}: $detail", error) + } + + private fun throwIfInvalid(errors: Collection, failures: Collection) { + if (errors.isEmpty() && failures.isEmpty()) return + val messages = errors + failures.mapNotNull { failure -> failure.message } + val combined = IllegalStateException(messages.sorted().joinToString(separator = "\n")) + failures.forEach { failure -> combined.addSuppressed(failure) } + throw combined + } + + private data class EntityDescriptor( + val entityClass: Class, + val id: ResourceLocation, + val codec: ForgeStreamCodec + ) + + private data class RendererDescriptor( + val entityClass: Class, + val rendererClass: Class<*>, + val constructor: Constructor<*> + ) { + fun toFactory(id: ResourceLocation): () -> RenderEntityRenderer = { + try { + @Suppress("UNCHECKED_CAST") + constructor.newInstance() as RenderEntityRenderer + } catch (error: InvocationTargetException) { + throw IllegalStateException( + "Failed to create RenderEntity renderer ${rendererClass.name} for $id", + error.targetException + ) + } catch (error: ReflectiveOperationException) { + throw IllegalStateException( + "Failed to create RenderEntity renderer ${rendererClass.name} for $id", + error + ) + } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/terrain/CooTerrainMappingRegion.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/terrain/CooTerrainMappingRegion.kt new file mode 100644 index 00000000..45d134b8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/terrain/CooTerrainMappingRegion.kt @@ -0,0 +1,237 @@ +package cn.coostack.cooparticlesapi.renderer.terrain + +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.phys.Vec3 +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.floor + +sealed interface CooTerrainMappingRegion { + val type: CooTerrainMappingRegionType + + fun contains(position: Vec3): Boolean + + fun encode(buffer: PacketByteBuf) { + buffer.writeVarInt(WIRE_VERSION) + buffer.writeResourceLocation(type.id) + when (this) { + is Sphere -> { + buffer.writeDouble(center.x) + buffer.writeDouble(center.y) + buffer.writeDouble(center.z) + buffer.writeDouble(radius) + } + is Box -> { + buffer.writeDouble(center.x) + buffer.writeDouble(center.y) + buffer.writeDouble(center.z) + buffer.writeDouble(halfExtents.x) + buffer.writeDouble(halfExtents.y) + buffer.writeDouble(halfExtents.z) + } + is Cylinder -> { + buffer.writeDouble(center.x) + buffer.writeDouble(center.y) + buffer.writeDouble(center.z) + buffer.writeDouble(radius) + buffer.writeDouble(height) + } + } + } + + fun bounds(): CooTerrainMappingBounds + + fun intersects( + minX: Int, + minY: Int, + minZ: Int, + maxX: Int, + maxY: Int, + maxZ: Int + ): Boolean = bounds().intersects(minX, minY, minZ, maxX, maxY, maxZ) + + data class Sphere(val center: Vec3, val radius: Double) : CooTerrainMappingRegion { + init { + require(center.x.isFinite() && center.y.isFinite() && center.z.isFinite()) { + "Terrain mapping sphere center must be finite" + } + require(radius.isFinite() && radius > 0.0) { + "Terrain mapping sphere radius must be finite and greater than zero" + } + } + + override val type: CooTerrainMappingRegionType = CooTerrainMappingRegionType.SPHERE + + override fun contains(position: Vec3): Boolean { + val dx = position.x - center.x + val dy = position.y - center.y + val dz = position.z - center.z + return dx * dx + dy * dy + dz * dz <= radius * radius + } + + override fun bounds(): CooTerrainMappingBounds { + return CooTerrainMappingBounds( + floor(center.x - radius).toInt(), + floor(center.y - radius).toInt(), + floor(center.z - radius).toInt(), + ceil(center.x + radius).toInt(), + ceil(center.y + radius).toInt(), + ceil(center.z + radius).toInt() + ) + } + + override fun intersects( + minX: Int, + minY: Int, + minZ: Int, + maxX: Int, + maxY: Int, + maxZ: Int + ): Boolean { + val nearestX = center.x.coerceIn(minX.toDouble(), maxX.toDouble()) + val nearestY = center.y.coerceIn(minY.toDouble(), maxY.toDouble()) + val nearestZ = center.z.coerceIn(minZ.toDouble(), maxZ.toDouble()) + val dx = center.x - nearestX + val dy = center.y - nearestY + val dz = center.z - nearestZ + return dx * dx + dy * dy + dz * dz <= radius * radius + } + } + + data class Box(val center: Vec3, val halfExtents: Vec3) : CooTerrainMappingRegion { + init { + require(center.x.isFinite() && center.y.isFinite() && center.z.isFinite()) { + "Terrain mapping box center must be finite" + } + require( + halfExtents.x.isFinite() && halfExtents.y.isFinite() && halfExtents.z.isFinite() && + halfExtents.x > 0.0 && halfExtents.y > 0.0 && halfExtents.z > 0.0 + ) { + "Terrain mapping box half extents must be finite and greater than zero" + } + } + + override val type: CooTerrainMappingRegionType = CooTerrainMappingRegionType.BOX + + override fun contains(position: Vec3): Boolean { + return abs(position.x - center.x) <= halfExtents.x && + abs(position.y - center.y) <= halfExtents.y && + abs(position.z - center.z) <= halfExtents.z + } + + override fun bounds(): CooTerrainMappingBounds { + return CooTerrainMappingBounds( + floor(center.x - halfExtents.x).toInt(), + floor(center.y - halfExtents.y).toInt(), + floor(center.z - halfExtents.z).toInt(), + ceil(center.x + halfExtents.x).toInt(), + ceil(center.y + halfExtents.y).toInt(), + ceil(center.z + halfExtents.z).toInt() + ) + } + } + + data class Cylinder(val center: Vec3, val radius: Double, val height: Double) : CooTerrainMappingRegion { + init { + require(center.x.isFinite() && center.y.isFinite() && center.z.isFinite()) { + "Terrain mapping cylinder center must be finite" + } + require(radius.isFinite() && radius > 0.0) { + "Terrain mapping cylinder radius must be finite and greater than zero" + } + require(height.isFinite() && height > 0.0) { + "Terrain mapping cylinder height must be finite and greater than zero" + } + } + + override val type: CooTerrainMappingRegionType = CooTerrainMappingRegionType.CYLINDER + + override fun contains(position: Vec3): Boolean { + val dx = position.x - center.x + val dz = position.z - center.z + return dx * dx + dz * dz <= radius * radius && + abs(position.y - center.y) <= height * 0.5 + } + + override fun bounds(): CooTerrainMappingBounds { + val halfHeight = height * 0.5 + return CooTerrainMappingBounds( + floor(center.x - radius).toInt(), + floor(center.y - halfHeight).toInt(), + floor(center.z - radius).toInt(), + ceil(center.x + radius).toInt(), + ceil(center.y + halfHeight).toInt(), + ceil(center.z + radius).toInt() + ) + } + + override fun intersects( + minX: Int, + minY: Int, + minZ: Int, + maxX: Int, + maxY: Int, + maxZ: Int + ): Boolean { + val halfHeight = height * 0.5 + if (center.y + halfHeight < minY || center.y - halfHeight > maxY) return false + val nearestX = center.x.coerceIn(minX.toDouble(), maxX.toDouble()) + val nearestZ = center.z.coerceIn(minZ.toDouble(), maxZ.toDouble()) + val dx = center.x - nearestX + val dz = center.z - nearestZ + return dx * dx + dz * dz <= radius * radius + } + } + + companion object { + private const val WIRE_VERSION = 2 + + fun decode(buffer: PacketByteBuf): CooTerrainMappingRegion { + val version = buffer.readVarInt() + require(version in 1..WIRE_VERSION) { "Unsupported terrain mapping region version: $version" } + val typeId = buffer.readResourceLocation() + val type = CooTerrainMappingRegionType.fromId(typeId) + require(version != 1 || type == CooTerrainMappingRegionType.SPHERE) { + "Terrain mapping region version 1 only supports sphere" + } + return when (type) { + CooTerrainMappingRegionType.SPHERE -> Sphere( + Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble()), + buffer.readDouble() + ) + CooTerrainMappingRegionType.BOX -> Box( + Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble()), + Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble()) + ) + CooTerrainMappingRegionType.CYLINDER -> Cylinder( + Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble()), + buffer.readDouble(), + buffer.readDouble() + ) + null -> error("Unknown terrain mapping region type: $typeId") + } + } + } +} + +data class CooTerrainMappingBounds( + val minX: Int, + val minY: Int, + val minZ: Int, + val maxX: Int, + val maxY: Int, + val maxZ: Int +) { + fun intersects( + minX: Int, + minY: Int, + minZ: Int, + maxX: Int, + maxY: Int, + maxZ: Int + ): Boolean { + return this.maxX >= minX && this.minX <= maxX && + this.maxY >= minY && this.minY <= maxY && + this.maxZ >= minZ && this.minZ <= maxZ + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorDouble.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorDouble.kt new file mode 100644 index 00000000..b74ee7d6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorDouble.kt @@ -0,0 +1,74 @@ +package cn.coostack.cooparticlesapi.utils.interpolator.data + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + + +class InterpolatorDouble(value: Double) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of(InterpolatorDouble>( + { buf, data -> + buf.writeDouble(data.value) + }, { + val current = it.readDouble() + InterpolatorDouble(current) + } + ) + } + + override fun getWithInterpolator(progress: Number): Double { + return GraphMathHelper.lerp(progress.toDouble(), last, value) + } + + override fun getCurrent(): Double { + return value + } + + operator fun plus(double: Double): InterpolatorDouble { + uploadData(value + double) + return this + } + + operator fun minus(double: Double): InterpolatorDouble { + uploadData(value - double) + return this + } + + operator fun times(double: Double): InterpolatorDouble { + uploadData(value * double) + return this + } + + operator fun div(double: Double): InterpolatorDouble { + require(double != 0.0) { "Division by zero" } + uploadData(value / double) + return this + } + + operator fun unaryMinus(): InterpolatorDouble { + uploadData(-value) + return this + } + + operator fun plus(other: InterpolatorDouble): InterpolatorDouble { + uploadData(value + other.value) + return this + } + + operator fun minus(other: InterpolatorDouble): InterpolatorDouble { + uploadData(value - other.value) + return this + } + + operator fun times(other: InterpolatorDouble): InterpolatorDouble { + uploadData(value * other.value) + return this + } + + operator fun div(other: InterpolatorDouble): InterpolatorDouble { + require(other.value != 0.0) { "Division by zero" } + uploadData(value / other.value) + return this + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorFloat.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorFloat.kt new file mode 100644 index 00000000..d6d5e89b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorFloat.kt @@ -0,0 +1,74 @@ +package cn.coostack.cooparticlesapi.utils.interpolator.data + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + + +class InterpolatorFloat(value: Float) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of(InterpolatorFloat>( + { buf, data -> + buf.writeFloat(data.value) + }, { + val current = it.readFloat() + InterpolatorFloat(current) + } + ) + } + + override fun getWithInterpolator(progress: Number): Float { + return GraphMathHelper.lerp(progress.toFloat(), last, value) + } + + override fun getCurrent(): Float { + return value + } + + operator fun plus(float: Float): InterpolatorFloat { + uploadData(value + float) + return this + } + + operator fun minus(float: Float): InterpolatorFloat { + uploadData(value - float) + return this + } + + operator fun times(float: Float): InterpolatorFloat { + uploadData(value * float) + return this + } + + operator fun div(float: Float): InterpolatorFloat { + require(float != 0f) { "Division by zero" } + uploadData(value / float) + return this + } + + operator fun unaryMinus(): InterpolatorFloat { + uploadData(-value) + return this + } + + operator fun plus(other: InterpolatorFloat): InterpolatorFloat { + uploadData(value + other.value) + return this + } + + operator fun minus(other: InterpolatorFloat): InterpolatorFloat { + uploadData(value - other.value) + return this + } + + operator fun times(other: InterpolatorFloat): InterpolatorFloat { + uploadData(value * other.value) + return this + } + + operator fun div(other: InterpolatorFloat): InterpolatorFloat { + require(other.value != 0f) { "Division by zero" } + uploadData(value / other.value) + return this + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorQuaternionf.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorQuaternionf.kt new file mode 100644 index 00000000..ac051e46 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorQuaternionf.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.utils.interpolator.data + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +import org.joml.Quaternionf + +class InterpolatorQuaternionf(value: Quaternionf) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of(InterpolatorQuaternionf>( + { buf, data -> + buf.writeFloat(data.value.x) + buf.writeFloat(data.value.y) + buf.writeFloat(data.value.z) + buf.writeFloat(data.value.w) + }, { + val x = it.readFloat() + val y = it.readFloat() + val z = it.readFloat() + val w = it.readFloat() + InterpolatorQuaternionf(Quaternionf(x, y, z, w)) + } + ) + } + + override fun getWithInterpolator(progress: Number): Quaternionf { + return GraphMathHelper.lerp(progress.toFloat(), last, value) + } + + override fun getCurrent(): Quaternionf { + return Quaternionf(value) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorRelativeLocation.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorRelativeLocation.kt new file mode 100644 index 00000000..546b3bab --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorRelativeLocation.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.utils.interpolator.data + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import cn.coostack.cooparticlesapi.utils.RelativeLocation + +import net.minecraft.world.phys.Vec3 + +class InterpolatorRelativeLocation(value: RelativeLocation) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of(InterpolatorRelativeLocation>( + { buf, data -> + buf.writeDouble(data.value.x) + buf.writeDouble(data.value.y) + buf.writeDouble(data.value.z) + }, { + val x = it.readDouble() + val y = it.readDouble() + val z = it.readDouble() + InterpolatorRelativeLocation(RelativeLocation(x, y, z)) + } + ) + } + + override fun getWithInterpolator(progress: Number): RelativeLocation { + return GraphMathHelper.lerp(progress.toDouble(), last, value) + } + + override fun getCurrent(): RelativeLocation { + return RelativeLocation(value.x, value.y, value.z) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVec3d.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVec3d.kt new file mode 100644 index 00000000..b06c7da3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVec3d.kt @@ -0,0 +1,54 @@ +package cn.coostack.cooparticlesapi.utils.interpolator.data + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +import net.minecraft.world.phys.Vec3 + +class InterpolatorVec3d(value: Vec3) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of(InterpolatorVec3d>( + { buf, data -> + buf.writeVec3(data.value) + }, { + val current = it.readVec3() + InterpolatorVec3d(current) + } + ) + } + + override fun getWithInterpolator(progress: Number): Vec3 { + return GraphMathHelper.lerp(progress.toDouble(), last.toVector3d(), value.toVector3d()) + } + + override fun getCurrent(): Vec3 { + return value + } + + operator fun plus(vec: Vec3): InterpolatorVec3d { + uploadData(value.add(vec.toVector3d())) + return this + } + + operator fun minus(vec: Vec3): InterpolatorVec3d { + uploadData(value.sub(vec.toVector3d())) + return this + } + + operator fun times(double: Double): InterpolatorVec3d { + uploadData(value.mul(double)) + return this + } + + operator fun div(double: Double): InterpolatorVec3d { + require(double != 0.0) { "Division by zero" } + uploadData(value.mul(1.0 / double)) + return this + } + + operator fun unaryMinus(): InterpolatorVec3d { + uploadData(value.mul(-1.0)) + return this + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVector3f.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVector3f.kt new file mode 100644 index 00000000..e5de0ded --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVector3f.kt @@ -0,0 +1,58 @@ +package cn.coostack.cooparticlesapi.utils.interpolator.data + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +import org.joml.Vector3f + +class InterpolatorVector3f(value: Vector3f) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of(InterpolatorVector3f>( + { buf, data -> + buf.writeFloat(data.value.x()) + buf.writeFloat(data.value.y()) + buf.writeFloat(data.value.z()) + }, { + val x = it.readFloat() + val y = it.readFloat() + val z = it.readFloat() + InterpolatorVector3f(Vector3f(x, y, z)) + } + ) + } + + override fun getWithInterpolator(progress: Number): Vector3f { + return GraphMathHelper.lerp(progress.toFloat(), last, value) + } + + override fun getCurrent(): Vector3f { + return value + } + + operator fun plus(vec: Vector3f): InterpolatorVector3f { + uploadData(Vector3f(value).add(vec)) + return this + } + + operator fun minus(vec: Vector3f): InterpolatorVector3f { + uploadData(Vector3f(value).sub(vec)) + return this + } + + operator fun times(float: Float): InterpolatorVector3f { + uploadData(Vector3f(value).mul(float)) + return this + } + + operator fun div(float: Float): InterpolatorVector3f { + require(float != 0f) { "Division by zero" } + uploadData(Vector3f(value).mul(1f / float)) + return this + } + + operator fun unaryMinus(): InterpolatorVector3f { + uploadData(Vector3f(value).mul(-1f)) + return this + } +} diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 00000000..5fe7c167 --- /dev/null +++ b/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader="javafml" +loaderVersion="[46,)" +license="GPL-3.0" + +[[mods]] +modId="cooparticlesapi" +version="${file.jarVersion}" +displayName="CooParticlesAPI" +authors="CooStack" +description=''' +CooParticlesAPI made by CooStack +''' + +[[dependencies.cooparticlesapi]] +modId="forge" +mandatory=true +versionRange="[46,)" +ordering="NONE" +side="BOTH" + +[[dependencies.cooparticlesapi]] +modId="minecraft" +mandatory=true +versionRange="[1.20.1,1.21)" +ordering="NONE" +side="BOTH" diff --git a/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ClientNetworking b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ClientNetworking new file mode 100644 index 00000000..f6a94a05 --- /dev/null +++ b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ClientNetworking @@ -0,0 +1 @@ +cn.coostack.cooparticlesapi.platform.ForgeClientNetworking diff --git a/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.CooRegistry b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.CooRegistry new file mode 100644 index 00000000..addf2d0d --- /dev/null +++ b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.CooRegistry @@ -0,0 +1 @@ +cn.coostack.cooparticlesapi.platform.ForgeRegistry diff --git a/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ServerNetworking b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ServerNetworking new file mode 100644 index 00000000..d826b39e --- /dev/null +++ b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ServerNetworking @@ -0,0 +1 @@ +cn.coostack.cooparticlesapi.platform.ForgeServerNetworking diff --git a/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.services.IPlatformHelper b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.services.IPlatformHelper new file mode 100644 index 00000000..908c814a --- /dev/null +++ b/forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.services.IPlatformHelper @@ -0,0 +1 @@ +cn.coostack.cooparticlesapi.platform.ForgePlatformHelper diff --git a/gradle.properties b/gradle.properties index aa5b5593..6505db70 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ version=2.5.6.3-SNAPSHOT group=cn.coostack artifact=cooparticlesapi -java_version=21 +java_version=17 # Common minecraft_version=1.21.1 mod_name=CooParticlesAPI @@ -25,8 +25,8 @@ fabric_version=0.115.1+1.21.1 fabric_loader_version=0.16.9 fabric_kotlin_version=1.13.4+kotlin.2.2.0 # Forge -forge_version=52.0.28 -forge_loader_version_range=[52,) +forge_version=1.20.1-46.0.14 +forge_loader_version_range=[46,) # NeoForge neoforge_version=21.1.200 neoforge_loader_version_range=[4,) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e18bc253..0e1ae8ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index 8af99515..893615e2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,44 +2,22 @@ pluginManagement { repositories { gradlePluginPortal() mavenCentral() - exclusiveContent { - forRepository { - maven { - name = 'Fabric' - url = uri('https://maven.fabricmc.net') - } - } - filter { - includeGroup('net.fabricmc') - includeGroup('fabric-loom') - } + maven { + name = 'Fabric' + url = uri('https://maven.fabricmc.net') } - exclusiveContent { - forRepository { - maven { - name = 'Sponge' - url = uri('https://repo.spongepowered.org/repository/maven-public') - } - } - filter { - includeGroupAndSubgroups("org.spongepowered") - } + maven { + name = 'Sponge' + url = uri('https://repo.spongepowered.org/repository/maven-public') } - exclusiveContent { - forRepository { - maven { - name = 'Forge' - url = uri('https://maven.minecraftforge.net') - } - } - filter { - includeGroupAndSubgroups('net.minecraftforge') - } + maven { + name = 'Forge' + url = uri('https://maven.minecraftforge.net') } } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' - } + } } plugins { @@ -51,4 +29,5 @@ plugins { rootProject.name = 'CooParticlesAPI' include('common') include('fabric') -include('neoforge') \ No newline at end of file +include('neoforge') +include('forge') \ No newline at end of file