From ae12cfff24d64e198f57f9babba88c4e7c6917a9 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 15:55:03 +0800 Subject: [PATCH 01/41] feat: add Forge 1.20.1 platform module - Add forge/ subproject with ForgeGradle 6.x config for MC 1.20.1 - Abstract common module APIs: CommonCodec replaces StreamCodec/FriendlyByteBuf - Update ServerNetworking/ClientNetworking interfaces to accept Any - Add Forge-specific packet managers, network channel, and event listeners - Port all Forge packet classes to 1.20.1 compatible APIs - Update neoforge networking implementations for new interfaces --- .../network/packet/api/CommonCodec.kt | 13 + .../network/buffer/BooleanControlerBuffer.kt | 40 + .../network/buffer/CharControlerBuffer.kt | 34 + .../network/buffer/DoubleControlerBuffer.kt | 33 + .../network/buffer/EmptyControlerBuffer.kt | 36 + .../network/buffer/FloatControlerBuffer.kt | 37 + .../network/buffer/IntArrayControlerBuffer.kt | 45 + .../network/buffer/IntControlerBuffer.kt | 35 + .../buffer/LongArrayControlerBuffer.kt | 46 + .../network/buffer/LongControlerBuffer.kt | 34 + .../buffer/NestedBuffersControlerBuffer.kt | 71 + .../buffer/ParticleControlerDataBuffer.kt | 32 + .../buffer/ParticleControlerDataBuffers.kt | 184 +++ .../buffer/RelativeLocationControlerBuffer.kt | 36 + .../network/buffer/ShortControlerBuffer.kt | 33 + .../network/buffer/StringControlerBuffer.kt | 33 + .../network/buffer/UUIDControlerBuffer.kt | 33 + .../network/buffer/Vec3dControlerBuffer.kt | 41 + .../network/packet/api/ClientContext.kt | 33 + .../packet/api/CooClientPacketManager.kt | 214 +++ .../network/packet/api/CooPacket.kt | 13 + .../network/packet/api/CooPacketKind.kt | 24 + .../packet/api/CooServerPacketManager.kt | 304 +++++ .../network/packet/api/ServerContext.kt | 33 + .../api/envelope/CooPacketEnvelopeC2S.kt | 43 + .../api/envelope/CooPacketEnvelopeS2C.kt | 43 + .../packet/client/PacketKeyActionC2S.kt | 50 + .../listener/ClientCameraShakeHandler.kt | 80 ++ .../listener/ClientClearStateHandler.kt | 13 + .../listener/ClientDataHolderPacketHandler.kt | 15 + .../ClientDisplayEntityPacketHandler.kt | 50 + .../ClientDisplayEntityStateHandler.kt | 19 + .../ClientKeyBindingCountdownHandler.kt | 15 + .../ClientParticleCompositionHandler.kt | 89 ++ .../ClientParticleCompositionRotateHandler.kt | 18 + .../ClientParticleCompositionStateHandler.kt | 20 + .../ClientParticleEmittersPacketHandler.kt | 75 ++ .../ClientParticleGroupPacketHandler.kt | 126 ++ .../listener/ClientParticlePacketHandler.kt | 32 + .../ClientParticleStylePacketHandler.kt | 94 ++ .../ClientRenderEntityPacketHandler.kt | 89 ++ .../ClientRendererPostEffectHandler.kt | 41 + .../listener/ClientSoundInstanceHandler.kt | 11 + .../client/listener/ClientSoundLoopHandler.kt | 11 + .../server/PacketBindStarfieldFboBlockS2C.kt | 67 + .../packet/server/PacketCameraShakeS2C.kt | 202 +++ .../server/PacketClearClientStateS2C.kt | 14 + .../server/PacketCooFxModelBlockTestS2C.kt | 6 + .../packet/server/PacketDataHolderS2C.kt | 101 ++ .../packet/server/PacketDisplayEntityS2C.kt | 31 + .../server/PacketDisplayEntityStateS2C.kt | 38 + .../server/PacketKeyBindingCountdownS2C.kt | 18 + .../packet/server/PacketParticleBatchS2C.kt | 37 + .../PacketParticleCompositionRotateS2C.kt | 29 + .../server/PacketParticleCompositionS2C.kt | 39 + .../PacketParticleCompositionStateS2C.kt | 41 + .../server/PacketParticleEmittersS2C.kt | 67 + .../packet/server/PacketParticleGroupS2C.kt | 100 ++ .../packet/server/PacketParticleS2C.kt | 35 + .../packet/server/PacketParticleStyleS2C.kt | 49 + .../packet/server/PacketRenderEntityS2C.kt | 50 + .../server/PacketRendererPostEffectS2C.kt | 86 ++ .../packet/server/PacketSoundInstanceS2C.kt | 234 ++++ .../packet/server/PacketSoundLoopS2C.kt | 74 ++ .../server/PacketTerrainEffectGroupS2C.kt | 440 ++++++ .../packet/server/PacketTerrainMappingS2C.kt | 186 +++ .../server/listener/ServerKeyActionHandler.kt | 17 + .../PacketPerformanceStatusControlS2C.kt | 38 + .../PacketPerformanceStatusRequestC2S.kt | 39 + .../PacketPerformanceStatusSnapshotS2C.kt | 191 +++ .../PacketOpenBoundTestSelectionScreenS2C.kt | 48 + .../PacketOpenTestControllerScreenS2C.kt | 123 ++ .../PacketReviewTestControllerC2S.kt | 46 + .../PacketSelectBoundTestControllerC2S.kt | 31 + .../testblock/PacketStartTestControllerC2S.kt | 31 + .../testblock/PacketStopTestControllerC2S.kt | 31 + .../PacketUpdateTestControllerC2S.kt | 80 ++ .../particle/SequencedServerParticleGroup.kt | 164 +++ .../network/particle/ServerParticleGroup.kt | 328 +++++ .../particle/ServerParticleGroupManager.kt | 200 +++ .../composition/AutoParticleComposition.kt | 18 + .../AutoSequencedParticleComposition.kt | 16 + .../particle/composition/CompositionData.kt | 51 + .../composition/ParticleComposition.kt | 1177 +++++++++++++++++ .../composition/ParticleShapeComposition.kt | 156 +++ .../SequencedParticleComposition.kt | 518 ++++++++ .../SequencedParticleShapeComposition.kt | 203 +++ .../manager/ParticleCompositionManager.kt | 379 ++++++ .../network/particle/data/DoubleRangeData.kt | 26 + .../network/particle/data/FloatRangeData.kt | 19 + .../network/particle/data/IntRangeData.kt | 24 + .../network/particle/data/RangeData.kt | 7 + .../network/particle/emitters/AutoEmitters.kt | 29 + .../particle/emitters/AutoParticleEmitters.kt | 32 + .../AutoTransformableCParticleEmitter.kt | 29 + .../emitters/CParticleEmitterSpace.kt | 34 + .../particle/emitters/ClassEmitters.kt | 411 ++++++ .../emitters/ClassParticleEmitters.kt | 594 +++++++++ .../emitters/CompositionEmittersData.kt | 117 ++ .../emitters/ControlableCParticleData.kt | 266 ++++ .../emitters/ControlableParticleData.kt | 458 +++++++ .../emitters/DisplayEntityEmittersData.kt | 144 ++ .../particle/emitters/ParticleDataFactory.kt | 5 + .../particle/emitters/ParticleEmitters.kt | 122 ++ .../emitters/ParticleEmittersManager.kt | 398 ++++++ .../particle/emitters/PhysicConstant.kt | 23 + .../emitters/SimpleRandomParticleData.kt | 270 ++++ .../emitters/TransformableCParticleEmitter.kt | 493 +++++++ .../particle/emitters/command/OrbitMode.kt | 30 + .../command/ParticleAttractionCommand.kt | 128 ++ .../emitters/command/ParticleCommand.kt | 46 + .../emitters/command/ParticleCommandQueue.kt | 120 ++ .../command/ParticleDistortionCommand.kt | 334 +++++ .../emitters/command/ParticleDragCommand.kt | 99 ++ .../command/ParticleFlowFieldCommand.kt | 119 ++ .../command/ParticleGravityCommand.kt | 20 + .../emitters/command/ParticleInheritMode.kt | 6 + .../command/ParticleInheritVelocityCommand.kt | 340 +++++ .../command/ParticleLifetimeMotionCommand.kt | 377 ++++++ .../command/ParticleLifetimeVelocityMode.kt | 7 + .../emitters/command/ParticleMotionSpace.kt | 6 + .../emitters/command/ParticleNoiseCommand.kt | 205 +++ .../emitters/command/ParticleOrbitCommand.kt | 177 +++ .../command/ParticleRotationForceCommand.kt | 98 ++ .../ParticleToroidalCirculationCommand.kt | 226 ++++ .../emitters/command/ParticleVortexCommand.kt | 161 +++ .../command/curve/BezierFloatKeyframe.kt | 10 + .../command/curve/BezierKeyframeFloatCurve.kt | 250 ++++ .../command/curve/ConstantFloatCurve.kt | 18 + .../emitters/command/curve/FloatCurve.kt | 22 + .../emitters/command/curve/FloatKeyframe.kt | 12 + .../command/curve/KeyframeFloatCurve.kt | 78 ++ .../command/curve/LinearFloatCurve.kt | 27 + .../environment/wind/BallWindDirection.kt | 77 ++ .../environment/wind/BoxWindDirection.kt | 92 ++ .../environment/wind/GlobalWindDirection.kt | 68 + .../environment/wind/WindDirection.kt | 46 + .../environment/wind/WindDirections.kt | 57 + .../emitters/event/ParticleCollideEvent.kt | 25 + .../particle/emitters/event/ParticleEvent.kt | 24 + .../emitters/event/ParticleEventHandler.kt | 26 + .../event/ParticleEventHandlerManager.kt | 56 + .../emitters/event/ParticleHitEntityEvent.kt | 23 + .../emitters/event/ParticleOnGroundEvent.kt | 34 + .../emitters/event/ParticleOnLiquidEvent.kt | 24 + .../emitters/type/BoxEmittersShootType.kt | 78 ++ .../emitters/type/EmittersShootType.kt | 34 + .../emitters/type/EmittersShootTypes.kt | 74 ++ .../emitters/type/LineEmittersShootType.kt | 53 + .../emitters/type/MathEmittersShootType.kt | 196 +++ .../emitters/type/PointEmittersShootType.kt | 48 + .../particle/style/ParticleGroupStyle.kt | 573 ++++++++ .../particle/style/ParticleShapeStyle.kt | 222 ++++ .../particle/style/ParticleStyleManager.kt | 259 ++++ .../particle/style/ParticleStyleProvider.kt | 13 + .../style/SequencedParticleShapeStyle.kt | 288 ++++ .../particle/style/SequencedParticleStyle.kt | 551 ++++++++ .../particle/util/ServerParticleUtil.kt | 111 ++ .../platform/ClientNetworking.kt | 5 + .../platform/ForgeNetworkChannel.kt | 59 + .../platform/ServerNetworking.kt | 15 + .../network/buffer/BooleanControlerBuffer.kt | 40 + .../network/buffer/CharControlerBuffer.kt | 34 + .../network/buffer/DoubleControlerBuffer.kt | 33 + .../network/buffer/EmptyControlerBuffer.kt | 36 + .../network/buffer/FloatControlerBuffer.kt | 37 + .../network/buffer/IntArrayControlerBuffer.kt | 45 + .../network/buffer/IntControlerBuffer.kt | 35 + .../buffer/LongArrayControlerBuffer.kt | 46 + .../network/buffer/LongControlerBuffer.kt | 34 + .../buffer/NestedBuffersControlerBuffer.kt | 71 + .../buffer/ParticleControlerDataBuffer.kt | 32 + .../buffer/ParticleControlerDataBuffers.kt | 184 +++ .../buffer/RelativeLocationControlerBuffer.kt | 36 + .../network/buffer/ShortControlerBuffer.kt | 33 + .../network/buffer/StringControlerBuffer.kt | 33 + .../network/buffer/UUIDControlerBuffer.kt | 33 + .../network/buffer/Vec3dControlerBuffer.kt | 41 + .../network/packet/api/ClientContext.kt | 33 + .../packet/api/CooClientPacketManager.kt | 250 ++++ .../network/packet/api/CooPacket.kt | 56 + .../network/packet/api/CooPacketKind.kt | 24 + .../packet/api/CooServerPacketManager.kt | 349 +++++ .../network/packet/api/ServerContext.kt | 33 + .../api/envelope/CooPacketEnvelopeC2S.kt | 52 + .../api/envelope/CooPacketEnvelopeS2C.kt | 58 + .../packet/client/PacketKeyActionC2S.kt | 56 + .../listener/ClientCameraShakeHandler.kt | 80 ++ .../listener/ClientClearStateHandler.kt | 13 + .../listener/ClientDataHolderPacketHandler.kt | 15 + .../ClientDisplayEntityPacketHandler.kt | 50 + .../ClientDisplayEntityStateHandler.kt | 19 + .../ClientKeyBindingCountdownHandler.kt | 15 + .../ClientParticleCompositionHandler.kt | 89 ++ .../ClientParticleCompositionRotateHandler.kt | 18 + .../ClientParticleCompositionStateHandler.kt | 20 + .../ClientParticleEmittersPacketHandler.kt | 75 ++ .../ClientParticleGroupPacketHandler.kt | 126 ++ .../listener/ClientParticlePacketHandler.kt | 32 + .../ClientParticleStylePacketHandler.kt | 94 ++ .../ClientRenderEntityPacketHandler.kt | 89 ++ .../ClientRendererPostEffectHandler.kt | 41 + .../listener/ClientSoundInstanceHandler.kt | 11 + .../client/listener/ClientSoundLoopHandler.kt | 11 + .../server/PacketBindStarfieldFboBlockS2C.kt | 67 + .../packet/server/PacketCameraShakeS2C.kt | 210 +++ .../server/PacketClearClientStateS2C.kt | 20 + .../server/PacketCooFxModelBlockTestS2C.kt | 6 + .../packet/server/PacketDataHolderS2C.kt | 109 ++ .../packet/server/PacketDisplayEntityS2C.kt | 40 + .../server/PacketDisplayEntityStateS2C.kt | 44 + .../server/PacketKeyBindingCountdownS2C.kt | 26 + .../packet/server/PacketParticleBatchS2C.kt | 43 + .../PacketParticleCompositionRotateS2C.kt | 37 + .../server/PacketParticleCompositionS2C.kt | 48 + .../PacketParticleCompositionStateS2C.kt | 47 + .../server/PacketParticleEmittersS2C.kt | 73 + .../packet/server/PacketParticleGroupS2C.kt | 106 ++ .../packet/server/PacketParticleS2C.kt | 40 + .../packet/server/PacketParticleStyleS2C.kt | 57 + .../packet/server/PacketRenderEntityS2C.kt | 60 + .../server/PacketRendererPostEffectS2C.kt | 92 ++ .../packet/server/PacketSoundInstanceS2C.kt | 243 ++++ .../packet/server/PacketSoundLoopS2C.kt | 83 ++ .../server/PacketTerrainEffectGroupS2C.kt | 440 ++++++ .../packet/server/PacketTerrainMappingS2C.kt | 186 +++ .../server/listener/ServerKeyActionHandler.kt | 17 + .../PacketPerformanceStatusControlS2C.kt | 38 + .../PacketPerformanceStatusRequestC2S.kt | 39 + .../PacketPerformanceStatusSnapshotS2C.kt | 191 +++ .../PacketOpenBoundTestSelectionScreenS2C.kt | 48 + .../PacketOpenTestControllerScreenS2C.kt | 123 ++ .../PacketReviewTestControllerC2S.kt | 46 + .../PacketSelectBoundTestControllerC2S.kt | 31 + .../testblock/PacketStartTestControllerC2S.kt | 31 + .../testblock/PacketStopTestControllerC2S.kt | 31 + .../PacketUpdateTestControllerC2S.kt | 80 ++ .../particle/SequencedServerParticleGroup.kt | 164 +++ .../network/particle/ServerParticleGroup.kt | 328 +++++ .../particle/ServerParticleGroupManager.kt | 200 +++ .../composition/AutoParticleComposition.kt | 18 + .../AutoSequencedParticleComposition.kt | 16 + .../particle/composition/CompositionData.kt | 51 + .../composition/ParticleComposition.kt | 1177 +++++++++++++++++ .../composition/ParticleShapeComposition.kt | 156 +++ .../SequencedParticleComposition.kt | 518 ++++++++ .../SequencedParticleShapeComposition.kt | 203 +++ .../manager/ParticleCompositionManager.kt | 379 ++++++ .../network/particle/data/DoubleRangeData.kt | 26 + .../network/particle/data/FloatRangeData.kt | 19 + .../network/particle/data/IntRangeData.kt | 24 + .../network/particle/data/RangeData.kt | 7 + .../network/particle/emitters/AutoEmitters.kt | 29 + .../particle/emitters/AutoParticleEmitters.kt | 32 + .../AutoTransformableCParticleEmitter.kt | 29 + .../emitters/CParticleEmitterSpace.kt | 34 + .../particle/emitters/ClassEmitters.kt | 411 ++++++ .../emitters/ClassParticleEmitters.kt | 594 +++++++++ .../emitters/CompositionEmittersData.kt | 117 ++ .../emitters/ControlableCParticleData.kt | 266 ++++ .../emitters/ControlableParticleData.kt | 458 +++++++ .../emitters/DisplayEntityEmittersData.kt | 144 ++ .../particle/emitters/ParticleDataFactory.kt | 5 + .../particle/emitters/ParticleEmitters.kt | 122 ++ .../emitters/ParticleEmittersManager.kt | 398 ++++++ .../particle/emitters/PhysicConstant.kt | 23 + .../emitters/SimpleRandomParticleData.kt | 270 ++++ .../emitters/TransformableCParticleEmitter.kt | 493 +++++++ .../particle/emitters/command/OrbitMode.kt | 30 + .../command/ParticleAttractionCommand.kt | 128 ++ .../emitters/command/ParticleCommand.kt | 46 + .../emitters/command/ParticleCommandQueue.kt | 120 ++ .../command/ParticleDistortionCommand.kt | 334 +++++ .../emitters/command/ParticleDragCommand.kt | 99 ++ .../command/ParticleFlowFieldCommand.kt | 119 ++ .../command/ParticleGravityCommand.kt | 20 + .../emitters/command/ParticleInheritMode.kt | 6 + .../command/ParticleInheritVelocityCommand.kt | 340 +++++ .../command/ParticleLifetimeMotionCommand.kt | 377 ++++++ .../command/ParticleLifetimeVelocityMode.kt | 7 + .../emitters/command/ParticleMotionSpace.kt | 6 + .../emitters/command/ParticleNoiseCommand.kt | 205 +++ .../emitters/command/ParticleOrbitCommand.kt | 177 +++ .../command/ParticleRotationForceCommand.kt | 98 ++ .../ParticleToroidalCirculationCommand.kt | 226 ++++ .../emitters/command/ParticleVortexCommand.kt | 161 +++ .../command/curve/BezierFloatKeyframe.kt | 10 + .../command/curve/BezierKeyframeFloatCurve.kt | 250 ++++ .../command/curve/ConstantFloatCurve.kt | 18 + .../emitters/command/curve/FloatCurve.kt | 22 + .../emitters/command/curve/FloatKeyframe.kt | 12 + .../command/curve/KeyframeFloatCurve.kt | 78 ++ .../command/curve/LinearFloatCurve.kt | 27 + .../environment/wind/BallWindDirection.kt | 77 ++ .../environment/wind/BoxWindDirection.kt | 92 ++ .../environment/wind/GlobalWindDirection.kt | 68 + .../environment/wind/WindDirection.kt | 46 + .../environment/wind/WindDirections.kt | 57 + .../emitters/event/ParticleCollideEvent.kt | 25 + .../particle/emitters/event/ParticleEvent.kt | 24 + .../emitters/event/ParticleEventHandler.kt | 26 + .../event/ParticleEventHandlerManager.kt | 56 + .../emitters/event/ParticleHitEntityEvent.kt | 23 + .../emitters/event/ParticleOnGroundEvent.kt | 34 + .../emitters/event/ParticleOnLiquidEvent.kt | 24 + .../emitters/type/BoxEmittersShootType.kt | 78 ++ .../emitters/type/EmittersShootType.kt | 34 + .../emitters/type/EmittersShootTypes.kt | 74 ++ .../emitters/type/LineEmittersShootType.kt | 53 + .../emitters/type/MathEmittersShootType.kt | 196 +++ .../emitters/type/PointEmittersShootType.kt | 48 + .../particle/style/ParticleGroupStyle.kt | 573 ++++++++ .../particle/style/ParticleShapeStyle.kt | 222 ++++ .../particle/style/ParticleStyleManager.kt | 259 ++++ .../particle/style/ParticleStyleProvider.kt | 13 + .../style/SequencedParticleShapeStyle.kt | 288 ++++ .../particle/style/SequencedParticleStyle.kt | 551 ++++++++ .../particle/util/ServerParticleUtil.kt | 111 ++ 318 files changed, 35691 insertions(+) create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CommonCodec.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt create mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt 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/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt new file mode 100644 index 00000000..0c548747 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt @@ -0,0 +1,40 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class BooleanControlerBuffer() : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "boolean" + ) + ) + } + + override var loadedValue: Boolean? = false + + override fun encode(value: Boolean): ByteArray { + return byteArrayOf(if (value) 1 else 0) + } + + + override fun encode(): ByteArray { + return encode(loadedValue ?: false) + } + + override fun decode(buf: ByteArray): Boolean { + if (buf.isNotEmpty()) { + return buf[0] == 1.toByte() + } + return false + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt new file mode 100644 index 00000000..9fd449e4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class CharControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "char" + ) + ) + } + override var loadedValue: Char? = ' ' + override fun encode(value: Char): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Char { + return String(buf)[0] + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt new file mode 100644 index 00000000..70e91e64 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class DoubleControlerBuffer() : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "double" + ) + ) + } + override var loadedValue: Double? = 0.0 + override fun encode(value: Double): ByteArray { + return value.toString().toByteArray() + } + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Double { + return String(buf).toDouble() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt new file mode 100644 index 00000000..fa82448f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt @@ -0,0 +1,36 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class EmptyControlerBuffer() : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "empty" + ) + ) + } + + override var loadedValue: Unit? = null + + override fun encode(value: Unit): ByteArray { + return byteArrayOf(0) + } + + + override fun encode(): ByteArray { + return byteArrayOf() + } + + override fun decode(buf: ByteArray) { + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt new file mode 100644 index 00000000..f538f1f4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt @@ -0,0 +1,37 @@ +package cn.coostack.cooparticlesapi.network.buffer + + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class FloatControlerBuffer() : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "float" + ) + ) + } + + override var loadedValue: Float? = 0.0f + override fun encode(value: Float): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Float { + return String(buf).toFloat() + + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt new file mode 100644 index 00000000..72236692 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt @@ -0,0 +1,45 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + +class IntArrayControlerBuffer : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "int_array" + ) + ) + } + + override var loadedValue: IntArray? = IntArray(0) + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun encode(value: IntArray): ByteArray { + val buffer = Unpooled.buffer() + buffer.writeInt(value.size) + value.forEach { buffer.writeInt(it) } + return buffer.copy().array() + } + + override fun decode(buf: ByteArray): IntArray { + val wrap = Unpooled.wrappedBuffer(buf) + val size = wrap.readInt() + val arr = IntArray(size) + for (i in 0 until size) { + arr[i] = wrap.readInt() + } + return arr + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt new file mode 100644 index 00000000..d2e89cf3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt @@ -0,0 +1,35 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class IntControlerBuffer : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "int" + ) + ) + } + + override var loadedValue: Int? = 0 + override fun encode(value: Int): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Int { + return String(buf).toInt() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt new file mode 100644 index 00000000..fcf69566 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + +class LongArrayControlerBuffer : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "long_array" + ) + ) + } + + override var loadedValue: LongArray? = LongArray(0) + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun encode(value: LongArray): ByteArray { + val buffer = Unpooled.buffer() + buffer.writeInt(value.size) + value.forEach { + buffer.writeLong(it) + } + return buffer.copy().array() + } + + override fun decode(buf: ByteArray): LongArray { + val wrap = Unpooled.wrappedBuffer(buf) + val size = wrap.readInt() + val arr = LongArray(size) + for (i in 0 until size) { + arr[i] = wrap.readLong() + } + return arr + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt new file mode 100644 index 00000000..c968c112 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class LongControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "long" + ) + ) + } + + override var loadedValue: Long? = 0L + override fun encode(value: Long): ByteArray { + return value.toString().toByteArray() + } + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Long { + return String(buf).toLong() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt new file mode 100644 index 00000000..4f40c30b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt @@ -0,0 +1,71 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + + +/** + * 如果某个style使用了嵌套 (内容使用了其他Style或者group) + * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer + * 如果你使用了自定义的ParticleControlableDataBuffer + * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe + */ +class NestedBuffersControlerBuffer : ParticleControlerDataBuffer>> { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "nested" + ) + ) + } + + override var loadedValue: Map>? = null + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun encode(value: Map>): ByteArray { + val buf = Unpooled.buffer(value.size * 8) + // 第一个存 接下来的参数个数 + // 第一个 int代表key的长度 + // 第二个 int代表value类型的字符串类的长度 + // 第三个 int代表value的长度 + buf.writeInt(value.size) + value.forEach { (key, value) -> + buf.writeInt(key.length) + buf.writeBytes(key.toByteArray()) + val id = "${value.getBufferID().value.namespace}:${value.getBufferID().value.path}" + buf.writeInt(id.length) + buf.writeBytes(id.toByteArray()) + val subBytes = value.encode()!! + buf.writeInt(subBytes.size) + buf.writeBytes(subBytes) + } + return buf.copy().array() + } + + override fun decode(buf: ByteArray): Map> { + val buf = Unpooled.copiedBuffer(buf) + val len = buf.readInt() + val res = mutableMapOf>() + for (i in 0 until len) { + val keyLen = buf.readInt() + val key = String(buf.readBytes(keyLen).copy().array()) + val typeLen = buf.readInt() + val idSplit = String(buf.readBytes(typeLen).copy().array()).split(":") + val id = ResourceLocation.fromNamespaceAndPath(idSplit[0], idSplit[1]) + val valueLen = buf.readInt() + val valueBytes = buf.readBytes(valueLen).copy().array() + val buffer = ParticleControlerDataBuffers.withIdDecode(id, valueBytes)!! + res[key] = buffer + } + return res + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt new file mode 100644 index 00000000..b18ca163 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt @@ -0,0 +1,32 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import net.minecraft.resources.ResourceLocation + + +interface ParticleControlerDataBuffer { + + data class Id(val value: ResourceLocation) { + companion object { + @JvmStatic + fun toID(string: String): Id { + val split = string.split(":") + if (split.size != 2) { + throw IllegalArgumentException("Invalid ID format: $string") + } + val namespace = split[0] + val id = split[1] + return Id(ResourceLocation.fromNamespaceAndPath(namespace, id)) + } + } + } + + var loadedValue: T? + fun encode(): ByteArray? + + fun encode(value: T): ByteArray + + fun decode(buf: ByteArray): T + + fun getBufferID(): Id + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt new file mode 100644 index 00000000..acdc1c72 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt @@ -0,0 +1,184 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +object ParticleControlerDataBuffers { + private val wrapperToPrimitive = mapOf, Class<*>>( + java.lang.Integer::class.java to Int::class.java, + java.lang.Double::class.java to Double::class.java, + java.lang.Long::class.java to Long::class.java, + java.lang.Float::class.java to Float::class.java, + java.lang.Boolean::class.java to Boolean::class.java, + java.lang.Character::class.java to Char::class.java, + java.lang.Byte::class.java to Byte::class.java, + java.lang.Short::class.java to Short::class.java + ) + val registerBuilder = HashMap>>() + val registerTypes = HashMap, ParticleControlerDataBuffer.Id>() + + fun boolean(value: Boolean): BooleanControlerBuffer = BooleanControlerBuffer().apply { loadedValue = (value) } + fun char(value: Char): CharControlerBuffer = CharControlerBuffer().apply { loadedValue = (value) } + fun string(value: String): StringControlerBuffer = StringControlerBuffer().apply { loadedValue = (value) } + fun int(value: Int): IntControlerBuffer = IntControlerBuffer().apply { loadedValue = (value) } + fun intArray(value: IntArray): IntArrayControlerBuffer = IntArrayControlerBuffer().apply { loadedValue = (value) } + fun double(value: Double): DoubleControlerBuffer = DoubleControlerBuffer().apply { loadedValue = (value) } + fun float(value: Float): FloatControlerBuffer = FloatControlerBuffer().apply { loadedValue = (value) } + fun long(value: Long): LongControlerBuffer = LongControlerBuffer().apply { loadedValue = (value) } + fun longArray(value: LongArray): LongArrayControlerBuffer = + LongArrayControlerBuffer().apply { loadedValue = (value) } + + fun short(value: Short): ShortControlerBuffer = ShortControlerBuffer().apply { loadedValue = (value) } + fun vec3d(value: Vec3): Vec3dControlerBuffer = Vec3dControlerBuffer().apply { loadedValue = (value) } + fun relative(value: RelativeLocation): RelativeLocationControlerBuffer = + RelativeLocationControlerBuffer().apply { loadedValue = (value) } + + fun uuid(value: UUID): UUIDControlerBuffer = UUIDControlerBuffer().apply { loadedValue = (value) } + fun empty(): EmptyControlerBuffer = EmptyControlerBuffer() + + /** + * 如果某个style使用了嵌套 (内容使用了其他Style或者group) + * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer + * 如果你使用了自定义的ParticleControlableDataBuffer + * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe + */ + fun nested(value: Map>): NestedBuffersControlerBuffer = + NestedBuffersControlerBuffer().apply { loadedValue = value } + + fun withType(value: Any, clazz: Class>): ParticleControlerDataBuffer<*> { + val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + bufferCodec.loadedValue = value + return bufferCodec + } + + fun fromBufferType(value: Any, clazz: Class<*>): ParticleControlerDataBuffer<*>? { + val targetClass = registerTypes[clazz]?.let { clazz } + ?: wrapperToPrimitive[clazz]?.takeIf { registerTypes.containsKey(it) } + ?: return null + + return registerTypes[targetClass]?.let { withId(it, value) } + } + + fun withDecode(buf: ByteArray, clazz: Class>): ParticleControlerDataBuffer<*> { + val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + bufferCodec.loadedValue = bufferCodec.decode(buf) + return bufferCodec + } + + + fun withId(id: ParticleControlerDataBuffer.Id, value: Any): ParticleControlerDataBuffer<*>? { + val clazz = registerBuilder[id] ?: return null + return withType(value, clazz) + } + + fun withIdDecode(id: ParticleControlerDataBuffer.Id, array: ByteArray): ParticleControlerDataBuffer<*>? { + val clazz = registerBuilder[id] ?: return null + return withDecode(array, clazz) + } + + fun withId(id: ResourceLocation, value: Any): ParticleControlerDataBuffer<*>? { + val identifier = ParticleControlerDataBuffer.Id(id) + val clazz = registerBuilder[identifier] ?: return null + return withType(value, clazz) + } + + fun withIdDecode(id: ResourceLocation, array: ByteArray): ParticleControlerDataBuffer<*>? { + val identifier = ParticleControlerDataBuffer.Id(id) + val clazz = registerBuilder[identifier] ?: return null + return withDecode(array, clazz) + } + + + fun register( + bufType: Class<*>, + id: ParticleControlerDataBuffer.Id, + type: Class> + ) { + registerBuilder[id] = type + registerTypes[bufType] = id + } + + fun encode(buffer: ParticleControlerDataBuffer): ByteArray { + val code = buffer.encode() ?: throw IllegalStateException("buffer encode value is null") + val decoderID = buffer::class.java.name + val byteBuf = Unpooled.buffer() + byteBuf.writeInt(decoderID.length) + byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) + byteBuf.writeBytes(code) + return byteBuf.copy().array() + } + + fun encode(value: T, buffer: ParticleControlerDataBuffer): ByteArray { + val code = buffer.encode(value) + val decoderID = buffer::class.java.name + val byteBuf = Unpooled.buffer() + byteBuf.writeInt(decoderID.length) + byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) + byteBuf.writeBytes(code) + return byteBuf.copy().array() + } + + fun decode(bytes: ByteArray): T { + // 要求目标编码器必须具有空构造函数 + val byteBuf = Unpooled.wrappedBuffer(bytes) + val len = byteBuf.readInt() + val toStringBytes = ByteArray(len) + byteBuf.readBytes(toStringBytes) + val decoderID = String(toStringBytes, Charsets.UTF_8) + val code = byteBuf.readBytes(byteBuf.readableBytes()) + val codeBytes = ByteArray(code.readableBytes()) + code.readBytes(codeBytes) + val clazz = Class.forName(decoderID) + val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + + return ins.decode(codeBytes) + } + + fun decodeToBuffer(bytes: ByteArray): ParticleControlerDataBuffer { + val buf = Unpooled.wrappedBuffer(bytes) + return decodeToBuffer(buf) + } + + fun decodeToBuffer(byteBuf: ByteBuf): ParticleControlerDataBuffer { + // 要求目标编码器必须具有空构造函数 + val len = byteBuf.readInt() + val toStringBytes = ByteArray(len) + byteBuf.readBytes(toStringBytes) + val decoderID = String(toStringBytes, Charsets.UTF_8) + val code = byteBuf.readBytes(byteBuf.readableBytes()) + val codeBytes = ByteArray(code.readableBytes()) + code.readBytes(codeBytes) + val clazz = Class.forName(decoderID) + val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + + return ins.apply { loadedValue = (ins.decode(codeBytes)) } + } + + + init { + register(Boolean::class.java, BooleanControlerBuffer.id, BooleanControlerBuffer::class.java) + register(Long::class.java, LongControlerBuffer.id, LongControlerBuffer::class.java) + register(Integer::class.java, IntControlerBuffer.id, IntControlerBuffer::class.java) + register(Double::class.java, DoubleControlerBuffer.id, DoubleControlerBuffer::class.java) + register(Float::class.java, FloatControlerBuffer.id, FloatControlerBuffer::class.java) + register(String::class.java, StringControlerBuffer.id, StringControlerBuffer::class.java) + register(IntArray::class.java, IntArrayControlerBuffer.id, IntArrayControlerBuffer::class.java) + register(LongArray::class.java, LongArrayControlerBuffer.id, LongArrayControlerBuffer::class.java) + register(UUID::class.java, UUIDControlerBuffer.id, UUIDControlerBuffer::class.java) + register(Vec3::class.java, Vec3dControlerBuffer.id, Vec3dControlerBuffer::class.java) + register( + RelativeLocation::class.java, + RelativeLocationControlerBuffer.id, + RelativeLocationControlerBuffer::class.java + ) + register(Short::class.java, ShortControlerBuffer.id, ShortControlerBuffer::class.java) + register(Unit::class.java, EmptyControlerBuffer.id, EmptyControlerBuffer::class.java) + register(Char::class.java, CharControlerBuffer.id, CharControlerBuffer::class.java) + register(Map::class.java, NestedBuffersControlerBuffer.id, NestedBuffersControlerBuffer::class.java) + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt new file mode 100644 index 00000000..0e4ad1f6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt @@ -0,0 +1,36 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + +class RelativeLocationControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "relative" + ) + ) + } + + override var loadedValue: RelativeLocation? = RelativeLocation() + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): RelativeLocation { + return RelativeLocation.fromBytes(buf) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + + override fun encode(value: RelativeLocation): ByteArray { + return value.toBytes() + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt new file mode 100644 index 00000000..cb9f978e --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class ShortControlerBuffer() : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "short" + ) + ) + } + override var loadedValue: Short? = 0 + override fun encode(value: Short): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Short { + return String(buf).toShort() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt new file mode 100644 index 00000000..70e63b35 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class StringControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "string" + ) + ) + } + override var loadedValue: String? = "" + override fun encode(value: String): ByteArray { + return value.toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): String { + return String(buf) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt new file mode 100644 index 00000000..2df22bab --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class UUIDControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "uuid" + ) + ) + } + override var loadedValue: UUID? = UUID.randomUUID() + override fun encode(value: UUID): ByteArray { + return value.toString().toByteArray() + } + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): UUID { + return UUID.fromString(String(buf)) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt new file mode 100644 index 00000000..8ff7da90 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt @@ -0,0 +1,41 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +class Vec3dControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "vec3d" + ) + ) + } + + override var loadedValue: Vec3? = Vec3.ZERO + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Vec3 { + val wrappedBuffer = Unpooled.wrappedBuffer(buf) + return Vec3(wrappedBuffer.readDouble(), wrappedBuffer.readDouble(), wrappedBuffer.readDouble()) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + + override fun encode(value: Vec3): ByteArray { + val buf = Unpooled.buffer() + buf.writeDouble(value.x) + buf.writeDouble(value.y) + buf.writeDouble(value.z) + return buf.copy().array() + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt new file mode 100644 index 00000000..4113c61f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import net.minecraft.client.Minecraft + +/** + * 客户端收到 [CooPacket] 时的上下文 + * + * @property packet 已解码的业务包 + * @property kind 包类型 (普通/请求/响应) + * @property correlationId 关联ID, 0 表示普通包 + * @property timeoutTicks 请求模式下的超时tick (来自发送端) + */ +class ClientContext( + val packet: CooPacket, + val kind: CooPacketKind, + val correlationId: Long, + val timeoutTicks: Int, +) { + val client: Minecraft = Minecraft.getInstance() + + /** + * 在请求模式下,向服务端回复一个响应包 + * + * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 + * - 使用本上下文携带的 [correlationId],服务端 request 注册的回调将匹配执行 + */ + fun reply(response: CooPacket) { + if (kind != CooPacketKind.REQUEST) { + throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") + } + CooClientPacketManager.replyInternal(response, correlationId) + } +} 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..7518244c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt @@ -0,0 +1,214 @@ +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.ForgeNetworkChannel +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() + private lateinit var channel: ForgeNetworkChannel + + fun init(channel: ForgeNetworkChannel) { + this.channel = channel + channel.registerEnvelopeS2C { envelope -> + handleS2C(envelope) + } + } + + @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("收到未知 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 异常: ${envelope.packetId}", e) + } + + if (kind == CooPacketKind.RESPONSE) { + val pendingEntry = pending.remove(envelope.correlationId) ?: return + if (!pendingEntry.expectType.isInstance(packet)) { + CooParticlesConstants.logger.warn( + "CooPacket 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" + ) + return + } + try { + pendingEntry.callback(packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket request 回调异常 (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 未注册, 无法发送: ${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 编码失败: ${packet::class.java.name}", e) + return false + } + val envelope = CooPacketEnvelopeC2S( + kindId = kind.id, + packetId = packet.id(), + correlationId = correlationId, + timeoutTicks = timeoutTicks, + data = data, + ) + channel.sendEnvelopeC2S(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..f61e54f4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistryHelper +import cn.coostack.cooparticlesapi.network.packet.api.CommonCodec +import net.minecraft.resources.ResourceLocation + +abstract class CooPacket { + abstract fun id(): ResourceLocation + open fun codec(): CommonCodec = + 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/CooPacketKind.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt new file mode 100644 index 00000000..63a52437 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +/** + * 数据包类型 + * + * - [NORMAL]: 普通包,无需响应 + * - [REQUEST]: 请求包,等待对端响应 + * - [RESPONSE]: 响应包,对应某个 REQUEST 的回执 + */ +enum class CooPacketKind { + NORMAL, + REQUEST, + RESPONSE; + + companion object { + private val values = entries.toTypedArray() + + fun fromId(id: Int): CooPacketKind { + return values.getOrNull(id) ?: NORMAL + } + } + + val id: Int get() = ordinal +} 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..30d33eb2 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt @@ -0,0 +1,304 @@ +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.ForgeNetworkChannel +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() + private lateinit var channel: ForgeNetworkChannel + + fun init(channel: ForgeNetworkChannel) { + this.channel = channel + channel.registerEnvelopeC2S { envelope, player -> + handleC2S(envelope, player) + } + } + + @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( + packet: CooPacket, + timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, + noinline onResponse: (R) -> Unit, + ): Long = request(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, 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() + } + } + 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( + "收到未知 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 异常: ${envelope.packetId}", e) + } + + if (kind == CooPacketKind.RESPONSE) { + val pendingEntry = pending.remove(envelope.correlationId) ?: return + if (!pendingEntry.expectType.isInstance(packet)) { + CooParticlesConstants.logger.warn( + "CooPacket 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" + ) + return + } + try { + pendingEntry.callback(sender, packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket request 回调异常 (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 未注册, 无法发送: ${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 编码失败: ${packet::class.java.name}", e) + return false + } + val envelope = CooPacketEnvelopeS2C( + kindId = kind.id, + packetId = packet.id(), + correlationId = correlationId, + timeoutTicks = timeoutTicks, + data = data, + ) + channel.sendTo(player, envelope) + PerformanceStatusNetworkMetrics.recordSent( + PerformanceStatusNetworkEndpoint.SERVER, + data.size, + ) + return true + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt new file mode 100644 index 00000000..5d1dd997 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import net.minecraft.server.level.ServerPlayer + +/** + * 服务端收到 [CooPacket] 时的上下文 + * + * @property sender 发送该包的玩家 + * @property packet 已解码的业务包 + * @property kind 包类型 (普通/请求/响应) + * @property correlationId 关联ID, 0 表示普通包 + * @property timeoutTicks 请求模式下的超时tick (来自发送端) + */ +class ServerContext( + val sender: ServerPlayer, + val packet: CooPacket, + val kind: CooPacketKind, + val correlationId: Long, + val timeoutTicks: Int, +) { + /** + * 在请求模式下,向客户端回复一个响应包 + * + * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 + * - 使用本上下文携带的 [correlationId],客户端 request 注册的回调将匹配执行 + */ + fun reply(response: CooPacket) { + if (kind != CooPacketKind.REQUEST) { + throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") + } + CooServerPacketManager.replyInternal(sender, response, correlationId) + } +} 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..798a85eb --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt @@ -0,0 +1,43 @@ +package cn.coostack.cooparticlesapi.network.packet.api.envelope + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +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, +) : CooPacket() { + + companion object { + private val ID = ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "coo_packet_envelope_c2s" + ) + + @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() + ) + } + } + + override fun id(): ResourceLocation = ID +} 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..e753501c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt @@ -0,0 +1,43 @@ +package cn.coostack.cooparticlesapi.network.packet.api.envelope + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +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, +) : CooPacket() { + + companion object { + private val ID = ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "coo_packet_envelope_s2c" + ) + + @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() + ) + } + } + + override fun id(): ResourceLocation = ID +} 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..ea2dba24 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt @@ -0,0 +1,50 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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)) + }) + } + + return payloadID \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt new file mode 100644 index 00000000..7d545a33 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt @@ -0,0 +1,80 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketCameraShakeS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.utils.ClientCameraUtil +import net.minecraft.world.phys.Vec3 + +object ClientCameraShakeHandler { + fun receive( + payload: PacketCameraShakeS2C, + context: ClientContext + ) { + when (payload.operation) { + PacketCameraShakeS2C.CameraOperation.SHAKE -> handleShake(payload, context) + PacketCameraShakeS2C.CameraOperation.SET_OFFSET -> { + if (payload.instant) { + ClientCameraUtil.setOffsetNow(payload.position, payload.yawOffset, payload.pitchOffset) + } else { + ClientCameraUtil.setOffset(payload.position, payload.yawOffset, payload.pitchOffset) + } + } + + PacketCameraShakeS2C.CameraOperation.RESET_OFFSET -> { + if (payload.instant) { + ClientCameraUtil.resetOffsetNow() + } else { + ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) + } + } + + PacketCameraShakeS2C.CameraOperation.FORCE_POSITION -> { + if (payload.instant) { + ClientCameraUtil.setForcedCameraPositionNow(payload.position) + } else { + ClientCameraUtil.setForcedCameraPosition(payload.position) + } + } + + PacketCameraShakeS2C.CameraOperation.RESET_FORCE_POSITION -> { + if (payload.instant) { + ClientCameraUtil.resetForcedCameraPositionNow() + } else { + ClientCameraUtil.resetForcedCameraPosition() + } + } + + PacketCameraShakeS2C.CameraOperation.RESET_ALL -> { + if (payload.instant) { + ClientCameraUtil.resetAllNow() + } else { + ClientCameraUtil.stopShakeCamera() + ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) + ClientCameraUtil.resetForcedCameraPosition() + } + } + } + } + + private fun handleShake(payload: PacketCameraShakeS2C, context: ClientContext) { + val range = payload.range + val player = context.player() + var amplitude = payload.amplitude + var frequency = payload.frequency + if (range > 0) { + val distance = player.position().distanceTo(payload.origin) + if (distance > range) { + return + } + if (payload.attenuateByDistance) { + val attenuation = (1.0 - distance / range).coerceIn(0.0, 1.0) + amplitude *= attenuation + frequency *= attenuation + } + } + if (amplitude <= 0.0 || frequency <= 0.0) { + return + } + ClientCameraUtil.startShakeCamera(payload.tick, amplitude, frequency) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt new file mode 100644 index 00000000..f0a997e6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesAPIClient +import cn.coostack.cooparticlesapi.network.packet.server.PacketClearClientStateS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientClearStateHandler { + fun receive(payload: PacketClearClientStateS2C, context: ClientContext) { + context.client().execute { + CooParticlesAPIClient.clearTransientClientState() + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt new file mode 100644 index 00000000..6e803e15 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.data.holder.DataHolderManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketDataHolderS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientDataHolderPacketHandler { + fun receive( + payload: PacketDataHolderS2C, + context: ClientContext + ) { + val entity = context.player().level().getEntity(payload.entityId) ?: return + DataHolderManager.applyClient(entity, payload) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt new file mode 100644 index 00000000..e34cb2cd --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt @@ -0,0 +1,50 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.display.DisplayEntity +import cn.coostack.cooparticlesapi.display.DisplayEntityManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.PacketByteBuf + +object ClientDisplayEntityPacketHandler { + fun receive( + payload: PacketDisplayEntityS2C, + context: ClientContext + ) { + context.client().execute { + apply(payload, context) + } + } + + private fun apply(payload: PacketDisplayEntityS2C, context: ClientContext) { + if (payload.removed) { + DisplayEntityManager.clientView.remove(payload.uuid) + return + } + val new = decodeData(payload) + new.world = context.player().level() + val old = DisplayEntityManager.clientView[payload.uuid] ?: let { + // 新建 + DisplayEntityManager.addClient(new) + return + } + // 更新 + old.update(new) + } + + private fun decodeData(payload: PacketDisplayEntityS2C): DisplayEntity { + val data = payload.data + val type = payload.type + val codec = DisplayEntityManager.registeredTypes[type]!! + val new = codec.decode( + PacketByteBuf( + Unpooled.wrappedBuffer(data), + Minecraft.getInstance().player!!.registryAccess() + ) + ) + return new + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt new file mode 100644 index 00000000..980fe036 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt @@ -0,0 +1,19 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.display.DisplayEntityManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityStateS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientDisplayEntityStateHandler { + fun receive(payload: PacketDisplayEntityStateS2C, context: ClientContext) { + context.client().execute { + DisplayEntityManager.clientView[payload.uuid]?.applyRemoteState( + payload.position, + payload.yaw, + payload.pitch, + payload.roll, + payload.scale, + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt new file mode 100644 index 00000000..9c36c138 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.key.CooKeyBindingManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketKeyBindingCountdownS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientKeyBindingCountdownHandler { + fun receive( + payload: PacketKeyBindingCountdownS2C, + context: ClientContext + ) { + CooKeyBindingManager.setCountdown(payload.key, payload.cd) + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt new file mode 100644 index 00000000..1ebeecce --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt @@ -0,0 +1,89 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionS2C +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.PacketByteBuf +import java.util.concurrent.ConcurrentHashMap + +object ClientParticleCompositionHandler { + private val missingCodecWarnings = ConcurrentHashMap.newKeySet() + private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() + + fun receive( + payload: PacketParticleCompositionS2C, + context: ClientContext + ) { + context.client().execute { + apply(payload, context) + } + } + + private fun apply(payload: PacketParticleCompositionS2C, context: ClientContext) { + val distanceRemove = payload.distanceRemove + // 优化新建 + var new: ParticleComposition? = null + val old = ParticleCompositionManager.clientView[payload.uuid] + if (!distanceRemove) { + // 新建 如果old为null 则直接作为新的添加在里面 + new = decodeData(payload) ?: return + if (new.controlUUID != payload.uuid && identityMismatchWarnings.add(payload.type)) { + CooParticlesConstants.logger.warn( + "Composition 网络 UUID 不一致,已采用数据包 UUID: type={}, packet={}, decoded={}", + payload.type, + payload.uuid, + new.controlUUID, + ) + } + new.controlUUID = payload.uuid + new.world = context.player().level() + } + if (old == null && !distanceRemove) { + ParticleCompositionManager.addClient(new!!) + // fix 当composition消散的时候 会抛出npe的问题 + return + } + val current = old ?: return + // new == null时 distanceRemove应该为true + if (distanceRemove) { + current.remove() + } else { + if (payload.recreate || current.canceled || !current.displayed) { + current.clear(true) + ParticleCompositionManager.addClient(new!!) + return + } + // new 不是null 且 old 不为null 更新old + current.update(new!!) + } + } + + private fun decodeData(payload: PacketParticleCompositionS2C): ParticleComposition? { + val data = payload.data + val type = payload.type + val codec = ParticleCompositionManager.registeredTypes[type] ?: run { + if (missingCodecWarnings.add(type)) { + CooParticlesConstants.logger.warn( + "收到未注册的 Composition codec: type={}, uuid={}", + type, + payload.uuid, + ) + } + return null + } + val buffer = PacketByteBuf( + Unpooled.wrappedBuffer(data), + Minecraft.getInstance().player!!.registryAccess() + ) + return try { + codec.decode(buffer) + } finally { + buffer.release() + } + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt new file mode 100644 index 00000000..ac7c7f34 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.extend.asRelative +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionRotateS2C +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientParticleCompositionRotateHandler { + fun receive( + payload: PacketParticleCompositionRotateS2C, + context: ClientContext + ) { + context.client().execute { + val composition = ParticleCompositionManager.clientView[payload.uuid] ?: return@execute + composition.applyRemoteRotation(payload.direction?.asRelative(), payload.rollDelta) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt new file mode 100644 index 00000000..70b29589 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt @@ -0,0 +1,20 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionStateS2C +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientParticleCompositionStateHandler { + fun receive(payload: PacketParticleCompositionStateS2C, context: ClientContext) { + context.client().execute { + ParticleCompositionManager.clientView[payload.uuid]?.applyRemoteState( + payload.position, + payload.visibleRange, + payload.scale, + payload.displayStatus, + payload.closedInterval, + payload.current, + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt new file mode 100644 index 00000000..18cd3fa2 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt @@ -0,0 +1,75 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleEmittersS2C +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmittersManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.PacketByteBuf +import java.util.concurrent.ConcurrentHashMap + +object ClientParticleEmittersPacketHandler { + private val missingCodecWarnings = ConcurrentHashMap.newKeySet() + private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() + + fun receive( + payload: PacketParticleEmittersS2C, + context: ClientContext + ) { + context.client().execute { + when (payload.type) { + PacketParticleEmittersS2C.PacketType.CREATE -> handleCreate(payload, context) + PacketParticleEmittersS2C.PacketType.CHANGE -> handleChange(payload, context) + PacketParticleEmittersS2C.PacketType.REMOVE -> handleRemove(payload) + } + } + } + + private fun handleCreate(payload: PacketParticleEmittersS2C, context: ClientContext) { + val emitters = decode(payload) ?: return + ParticleEmittersManager.createClient(emitters, context.player().level()) + } + + private fun handleChange(payload: PacketParticleEmittersS2C, context: ClientContext) { + val emitters = decode(payload) ?: return + ParticleEmittersManager.changeClient(emitters, context.player().level()) + } + + private fun handleRemove(payload: PacketParticleEmittersS2C) { + ParticleEmittersManager.removeClient(payload.emitterUUID) + } + + private fun decode(payload: PacketParticleEmittersS2C): ParticleEmitters? { + val codec = ParticleEmittersManager.getCodecFromID(payload.emitterID) ?: run { + if (missingCodecWarnings.add(payload.emitterID)) { + CooParticlesConstants.logger.warn( + "收到未注册的 Emitter codec: id={}, uuid={}", + payload.emitterID, + payload.emitterUUID, + ) + } + return null + } + val buffer = PacketByteBuf( + Unpooled.wrappedBuffer(payload.emitterData), + Minecraft.getInstance().level!!.registryAccess(), + ) + return try { + codec.decode(buffer).also { emitters -> + if (emitters.uuid != payload.emitterUUID && identityMismatchWarnings.add(payload.emitterID)) { + CooParticlesConstants.logger.warn( + "Emitter 网络 UUID 不一致,已采用数据包 UUID: id={}, packet={}, decoded={}", + payload.emitterID, + payload.emitterUUID, + emitters.uuid, + ) + } + emitters.uuid = payload.emitterUUID + } + } finally { + buffer.release() + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt new file mode 100644 index 00000000..62939ddd --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt @@ -0,0 +1,126 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.particles.control.group.ClientParticleGroupManager +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.particles.control.group.SequencedParticleGroup +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.Minecraft +import net.minecraft.world.phys.Vec3 +import java.util.UUID + + +object ClientParticleGroupPacketHandler { + fun receive(packet: PacketParticleGroupS2C, context: ClientContext) { + // Group UUID + val uuid = packet.uuid + // 修改类型 + val type = packet.type + when (type) { + ControlType.CREATE -> handleCreate(uuid, packet.args) + ControlType.CHANGE -> handleChange(uuid, packet.args) + ControlType.REMOVE -> handleRemove(uuid) + } + } + + + private fun handleCreate( + groupUUID: UUID, + args: Map> + ) { + val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]?.loadedValue!! as Vec3 + val world = Minecraft.getInstance().level!! + val type = args[PacketParticleGroupS2C.PacketArgsType.GROUP_TYPE.ofArgs]!!.loadedValue as String + val currentTick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue as Int + val maxTick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue as Int + val scale = args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue as Double + val groupClass = Class.forName(type) + val builder = ClientParticleGroupManager.getBuilder(groupClass as Class)!! + val group = builder.createGroup(groupUUID, args) + if (group is SequencedParticleGroup) { + handleSequencedGroupArgs(group, args) + } + group.tick = currentTick + group.maxTick = maxTick + group.scale(scale) + group.display(pos, world) + ClientParticleGroupManager.addVisibleGroup(group) + } + + private fun handleChange( + groupUUID: UUID, + args: Map> + ) { + val targetGroup = ClientParticleGroupManager.getControlGroup(groupUUID) ?: return + val argKeys = args.keys + if (PacketParticleGroupS2C.PacketArgsType.POS.ofArgs in argKeys) { + val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]!!.loadedValue!! as Vec3 + targetGroup.teleportGroupTo(pos) + } + if (PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs in argKeys) { + val to = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs]!!.loadedValue!! as Vec3 + targetGroup.rotateToPoint(RelativeLocation.of(to)) + } + if (PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs in argKeys) { + val angle = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs]!!.loadedValue as Double + targetGroup.rotateAsAxis(angle) + } + if (PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs in argKeys) { + val axis = args[PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs]!!.loadedValue!! as Vec3 + targetGroup.axis = RelativeLocation.of(axis) + } + if (PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs in argKeys) { + val tick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue!! as Int + targetGroup.tick = tick + } + if (PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs in argKeys) { + val tick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue!! as Int + targetGroup.maxTick = tick + } + if (PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs in argKeys) { + val targetMethodName = + args[PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs]!!.loadedValue!! as String + targetGroup::class.java.getDeclaredMethod(targetMethodName).apply { + isAccessible = true + }.invoke(targetGroup) + } + + if (PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs in argKeys) { + targetGroup.scale( + args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue!! as Double + ) + } + if (targetGroup is SequencedParticleGroup) { + handleSequencedGroupArgs(targetGroup, args) + } + + val builder = ClientParticleGroupManager.getBuilder(targetGroup::class.java) ?: return + builder.changeGroup(targetGroup, args) + } + + private fun handleRemove(groupUUID: UUID) { + ClientParticleGroupManager.removeVisible(groupUUID) + } + + private fun handleSequencedGroupArgs( + group: SequencedParticleGroup, + args: Map> + ) { + if (args.containsKey("toggle")) { + group.toggle(args["toggle"]!!.loadedValue as Int) + } + if (args.containsKey("addCount")) { + group.addMultiple(args["addCount"]!!.loadedValue as Int) + } + if (args.containsKey("removeCount")) { + group.removeMultiple(args["removeCount"]!!.loadedValue as Int) + } + + if (args.containsKey("toggle_status")) { + group.toggleStatus(args["toggle_status"]!!.loadedValue as LongArray) + } + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt new file mode 100644 index 00000000..e55ab4d1 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt @@ -0,0 +1,32 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientParticlePacketHandler { + fun receive(packet: PacketParticleBatchS2C, context: ClientContext) { + val world = context.player().level() + packet.positions.forEach { position -> + world.addParticle( + packet.type, + true, + position.x, + position.y, + position.z, + packet.velocity.x, + packet.velocity.y, + packet.velocity.z, + ) + } + } + + fun receive(packet: PacketParticleS2C, context: ClientContext) { + val player = context.player() + val world = player.level() + world.addParticle( + packet.type, true, packet.pos.x, packet.pos.y, packet.pos.z, + packet.velocity.x, packet.velocity.y, packet.velocity.z, + ) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt new file mode 100644 index 00000000..66bacfc1 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt @@ -0,0 +1,94 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C +import cn.coostack.cooparticlesapi.network.particle.style.ParticleGroupStyle +import cn.coostack.cooparticlesapi.network.particle.style.ParticleStyleManager +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.Minecraft +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +object ClientParticleStylePacketHandler { + fun receive(packet: PacketParticleStyleS2C, context: ClientContext) { + // Group UUID + val uuid = packet.uuid + // 修改类型 + val type = packet.type + when (type) { + ControlType.CREATE -> handleCreate(uuid, packet.args) + ControlType.CHANGE -> handleChange(uuid, packet.args) + ControlType.REMOVE -> handleRemove(uuid) + } + } + + private fun handleRemove(uuid: UUID) { + ParticleStyleManager.clientViewStyles[uuid]?.remove() + } + + /** 处理客户端同步修改 */ + private fun handleChange( + uuid: UUID, + args: Map> + ) { + // 确保他在 clientView + val style = ParticleStyleManager.clientViewStyles[uuid] ?: return + if (args.containsKey("pos")) { + val pos = args["pos"]!!.loadedValue as Vec3 + style.teleportTo(pos) + } + if (args.containsKey("teleport")) { + val pos = args["teleport"]!!.loadedValue as Vec3 + style.teleportTo(pos) + } + if (args.containsKey("rotate_to") && args.containsKey("rotate_angle")) { + style.rotateToWithAngle( + args["rotate_to"]!!.loadedValue as RelativeLocation, + args["rotate_angle"]!!.loadedValue as Double + ) + } else { + if (args.containsKey("rotate_angle")) { + style.rotateAsAxis(args["rotate_angle"]!!.loadedValue as Double) + } + if (args.containsKey("rotate_to")) { + style.rotateToPoint(args["rotate_to"]!!.loadedValue as RelativeLocation) + } + } + if (args.containsKey("axis")) { + style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) + } + if (args.containsKey("scale")) { + style.scale = args["scale"]!!.loadedValue as Double + } + if (args.containsKey("lastUpdatedGameTime")) { + style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long + } + if (args.containsKey("displayedTime")) { + style.displayedTime = args["displayedTime"]!!.loadedValue as Long + } + style.readPacketArgs(args) + } + + /** 处理客户端同步创建 */ + private fun handleCreate( + uuid: UUID, + args: Map> + ) { + val clazz = args["style_type"]!!.loadedValue as String + val builderType = Class.forName(clazz) + val builder = ParticleStyleManager.getBuilder(builderType as Class) ?: return + val style = builder.createStyle(uuid, args) + val pos = args["pos"]!!.loadedValue as Vec3 + style.rotate = args["rotate"]!!.loadedValue as Double + style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) + style.scale = args["scale"]!!.loadedValue as Double + style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long + style.displayedTime = args["displayedTime"]!!.loadedValue as Long + val world = Minecraft.getInstance().level + style.readPacketArgs(args) + ParticleStyleManager.spawnStyle(world!!, pos, style) + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt new file mode 100644 index 00000000..b323f3f5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt @@ -0,0 +1,89 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.coofx.server.CooFxSceneRenderEntity +import cn.coostack.cooparticlesapi.network.packet.server.PacketRenderEntityS2C +import cn.coostack.cooparticlesapi.coofx.client.CooFxSceneClientRegistry +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.renderer.client.ClientRenderEntityManager +import cn.coostack.cooparticlesapi.renderer.runtime.ClientRenderEntityRegistry +import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityInstance +import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityRenderer +import cn.coostack.cooparticlesapi.renderer.RenderEntity +import io.netty.buffer.Unpooled +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation + +/** 把服务端 RenderEntity 同步包应用到客户端实体运行时。 */ +object ClientRenderEntityPacketHandler { + /** + * 解码同步数据,并在客户端线程执行创建、更新或移除操作。 + * + * @param packet 服务端发送的 RenderEntity 操作和序列化数据 + * @param context 提供客户端线程与当前客户端实例的网络上下文 + */ + fun receive( + packet: PacketRenderEntityS2C, + context: ClientContext + ) { + val method = packet.method + val data = packet.entityData + val id = packet.id + val buf = PacketByteBuf(Unpooled.wrappedBuffer(data)) + val type = ClientRenderEntityRegistry.get(id) + if (type == null) { + CooParticlesConstants.logger.error( + "[CooFX-CREATE] 客户端 RenderEntity 类型未注册:method=$method, packetUuid=${packet.uuid}, typeId=$id", + ) + return + } + val entity = type.codec.decode(buf) + context.client().execute { + entity.world = context.client().level + when (method) { + PacketRenderEntityS2C.Method.CREATE -> { + val renderer = resolveRenderer(entity, id) + val instance = RenderEntityInstance(entity, renderer) + ClientRenderEntityManager.add(instance) + (entity as? CooFxSceneRenderEntity)?.let { created -> + CooFxSceneClientRegistry.onCreated(created) + } + } + + PacketRenderEntityS2C.Method.TOGGLE -> { + val instance = ClientRenderEntityManager.getFrom(packet.uuid) + instance?.updateFrom(entity) + (instance?.entity as? CooFxSceneRenderEntity)?.let { updated -> + CooFxSceneClientRegistry.onUpdated(updated) + } + } + + PacketRenderEntityS2C.Method.REMOVE -> { + ClientRenderEntityManager.getFrom(packet.uuid)?.markRemoved() + CooFxSceneClientRegistry.onRemoved(packet.uuid) + } + } + } + } + + /** + * 解析实体使用的 renderer。旧式实体自带 renderer 时保留实例行为,V2 类型从注册表取共享实例。 + * + * @param entity 已解码的客户端实体 + * @param id 实体类型注册 id + * @return 可渲染当前实体的 renderer + * @throws IllegalStateException 类型没有注册 renderer 时抛出 + */ + @Suppress("UNCHECKED_CAST") + private fun resolveRenderer( + entity: RenderEntity, + id: ResourceLocation + ): RenderEntityRenderer { + if (entity is RenderEntityRenderer<*>) { + return entity as RenderEntityRenderer + } + val renderer = ClientRenderEntityRegistry.resolveRenderer(id) + ?: throw IllegalStateException("RenderEntity renderer not registered: $id") + return renderer as RenderEntityRenderer + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt new file mode 100644 index 00000000..b447f9e3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt @@ -0,0 +1,41 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.server.PacketRendererPostEffectS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.renderer.post.CooPostEffects +import cn.coostack.cooparticlesapi.renderer.post.PostEffectRuntimeRegistry + +object ClientRendererPostEffectHandler { + fun receive(packet: PacketRendererPostEffectS2C, context: ClientContext) { + context.client().execute { + apply(packet) + } + } + + internal fun apply(packet: PacketRendererPostEffectS2C) { + when (packet.operation) { + PacketRendererPostEffectS2C.Operation.CREATE, + PacketRendererPostEffectS2C.Operation.UPDATE -> applyState(packet) + PacketRendererPostEffectS2C.Operation.REMOVE -> CooPostEffects.client.remove(packet.instanceId) + } + } + + private fun applyState(packet: PacketRendererPostEffectS2C) { + val state = packet.state ?: return + if (!PostEffectRuntimeRegistry.containsType(state.effectType)) { + CooParticlesConstants.logger.warn( + "Skipping synced post effect id={} because type={} is not registered on the client", + state.instanceId, + state.effectType + ) + return + } + val instance = state.instantiate(serverSynced = true) ?: return + when (packet.operation) { + PacketRendererPostEffectS2C.Operation.CREATE -> CooPostEffects.client.add(instance) + PacketRendererPostEffectS2C.Operation.UPDATE -> CooPostEffects.client.update(instance) + PacketRendererPostEffectS2C.Operation.REMOVE -> Unit + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt new file mode 100644 index 00000000..34d09f43 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundInstanceS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.supports.sound.ClientSoundManager + +object ClientSoundInstanceHandler { + fun receive(payload: PacketSoundInstanceS2C, context: ClientContext) { + ClientSoundManager.handle(payload) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt new file mode 100644 index 00000000..613486cc --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundLoopS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.supports.sound.ClientSoundLoopManager + +object ClientSoundLoopHandler { + fun receive(payload: PacketSoundLoopS2C, context: ClientContext) { + ClientSoundLoopManager.handle(payload) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt new file mode 100644 index 00000000..32a3b9b2 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt @@ -0,0 +1,67 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.renderer.pipeline.CooBlockPipelines +import cn.coostack.cooparticlesapi.test.options.renderer.pipeline.RenderPipelineExamples +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.state.BlockState +import java.util.UUID + +/** + * 把方块测试选中的方块类型同步为客户端星空 FBO 临时绑定。 + * + * BlockState 只用于通过注册表同步方块类型。每次测试都有独立的 [bindingId],结束时只撤销 + * 自己的规则,之前的永久绑定或其他测试绑定会继续生效。 + */ +@CooAutoRegister +class PacketBindStarfieldFboBlockS2C() : CooPacket() { + /** 用于同步目标方块类型的状态载体。 */ + @CodecField + var state: BlockState = Blocks.AIR.defaultBlockState() + + /** 当前临时绑定的所有者 ID。 */ + @CodecField + var bindingId: UUID = UUID(0L, 0L) + + /** `true` 添加临时绑定,`false` 撤销该所有者的绑定。 */ + @CodecField + var enabled: Boolean = true + + /** + * @param state 测试位置的方块状态,用于传递方块类型 + * @param bindingId 当前测试的临时绑定 ID + * @param enabled 是否启用星空 FBO 绑定 + */ + constructor(state: BlockState, bindingId: UUID, enabled: Boolean) : this() { + this.state = state + this.bindingId = bindingId + this.enabled = enabled + } + + /** @return 星空 FBO 方块类型绑定包的稳定资源 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** + * @param context 当前客户端网络上下文 + */ + override fun onClientReceive(context: ClientContext) { + context.client.execute { + if (enabled) { + CooBlockPipelines.bindScoped(bindingId, state.block, RenderPipelineExamples.STARFIELD_BLOCK) + } else { + CooBlockPipelines.unbindScoped(bindingId) + } + } + } + + companion object { + private const val PACKET_ID = "bind_starfield_fbo_block_s2c" + } +} 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..1aa48a6b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt @@ -0,0 +1,202 @@ +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(identifierID) + + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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.values().firstOrNull { it.id == id } ?: CameraOperation.SHAKE + } + } \ No newline at end of file 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..399e58d3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt @@ -0,0 +1,14 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt new file mode 100644 index 00000000..db295522 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +/** + * 旧版 BlockTest 的一次性 CooFX S2C 包已由服务端权威 `CooFxSceneManager` 和 RenderEntity + * CREATE/TOGGLE/REMOVE 协议替代。本文件保留迁移说明,避免再次引入测试专用网络状态。 + */ 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..41d9b136 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt @@ -0,0 +1,101 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.data.holder.DataHolder +import cn.coostack.cooparticlesapi.data.holder.DataHolderKey +import cn.coostack.cooparticlesapi.data.holder.DataHolderManager +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.Entity + +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(id) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.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: Entity, 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: Entity): ByteArray { + val codec = DataHolderManager.getCodecFromID(type) + ?: throw IllegalStateException("DataHolder codec not registered for type: $type") + val buf = PacketByteBuf( + Unpooled.buffer(), + entity.level().registryAccess() + ) + @Suppress("UNCHECKED_CAST") + (codec as ).encode(buf, value) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + return data + } + } + + fun decodeData(): Map, Any> { + val entity = Minecraft.getInstance().level?.getEntity(entityId) + ?: return emptyMap() + return entries.associate { entry -> + val keyType = runCatching { + Class.forName(entry.type) + }.getOrDefault(Any::class.java) + val key = DataHolderKey.ofRaw(keyType, ResourceLocation.parse(entry.key)) + key to decodeValue(entry.type, entry.data, entity) + } + } + + private fun decodeValue(type: String, data: ByteArray, entity: Entity): Any { + val codec = DataHolderManager.getCodecFromID(type) + ?: throw IllegalStateException("DataHolder codec not registered for type: $type") + val buf = PacketByteBuf( + Unpooled.wrappedBuffer(data), + Minecraft.getInstance().connection?.registryAccess() + ?: entity.level().registryAccess() + ) + @Suppress("UNCHECKED_CAST") + return (codec as cn.coostack.cooparticlesapi.network.packet.api.CommonCodec).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..182f6689 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt @@ -0,0 +1,31 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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) + }) + } \ No newline at end of file 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..14ad0ebc --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt @@ -0,0 +1,38 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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..d2df37fa --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt @@ -0,0 +1,18 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + buf.writeInt(packet.cd) + }, { buf -> + PacketKeyBindingCountdownS2C(buf.readResourceLocation(), buf.readInt()) + }) + } \ No newline at end of file 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..61b94026 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt @@ -0,0 +1,37 @@ +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(identifierID) + val CODEC: = + 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..79a52a88 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt @@ -0,0 +1,29 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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) + }) + } \ No newline at end of file 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..d71ff8e8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.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 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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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 + } + }) + } \ No newline at end of file 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..66108d6f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt @@ -0,0 +1,41 @@ +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(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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..df1458ef --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt @@ -0,0 +1,67 @@ +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 +) { + /** + * 发射器同步包的生命周期操作。 + * + * [CREATE] 用于首次进入玩家可见范围,客户端必须创建并启动新实例; + * [CHANGE] 只更新已经存在的客户端实例,不得重新执行启动逻辑; + * [REMOVE] 按 UUID 立即清理客户端实例,不携带完整发射器状态。 + * 数字 ID 是网络协议的一部分,其中 `0` 和 `1` 保留旧协议的创建与移除编号。 + */ + 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(id) + + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.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..f601a7de --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt @@ -0,0 +1,100 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.particles.control.ControlType +import net.minecraft.network.PacketByteBuf + +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +/** + * 三个参数 + * @param uuid 更改的粒子组的UUID + * @param type 三种类型对应三种不同的包参数 + * 1. REMOVE args什么都不写 + * 2. CREATE args输入 + * 1. pos: Vec3d + * 2. groupType: Class + * 3. currentTick: Int + * 4. maxTick: Int + * 5. ByteBuf 用于ControlableParticleGroup 构建的输入参数(不包括自身的UUID) + * 6. scale 缩放大小 + * 3. CHANGE args输入支持 + * 1. pos: Vec3d + * 2. rotateTo: Vec3d + * 3. rotateAxis: Double / Float + * 4. axis: Vec3d + * 5. currentTick: Int + * 6. maxTick: Int + * 7. invoke: 无参方法名 + * 8. scale 缩放大小 + */ +class PacketParticleGroupS2C( + val uuid: UUID, + val type: 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" -> return POS + "current_tick" -> return CURRENT_TICK + "max_tick" -> return MAX_TICK + "rotate_to" -> return ROTATE_TO + "rotate_axis" -> return ROTATE_AXIS + "invoke" -> return INVOKE + "scale" -> return SCALE + "groupType" -> return GROUP_TYPE + else -> INVOKE + } + } + } + } + + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_group") + val payloadID = ResourceLocation(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, packet -> + buf.writeUUID(packet.uuid) + buf.writeInt(packet.type.id) + packet.args.forEach { (t, u) -> + val encode = 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 = 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 = ParticleControlerDataBuffers.decodeToBuffer(value) + args[key] = decode + } + PacketParticleGroupS2C( + uuid, type, args + ) + }) + + } +} \ No newline at end of file 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..1803fb75 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt @@ -0,0 +1,35 @@ +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 PacketParticleS2C( + val type: ParticleOptions, + val pos: Vec3, + val velocity: Vec3, +) { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle") + val payloadID = ResourceLocation(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, packet -> + buf.writeVec3(packet.pos) + buf.writeVec3(packet.velocity) + ParticleTypes.STREAM_CODEC.encode(buf, packet.type) + }, { buf -> + val pos = buf.readVec3() + val velocity = buf.readVec3() + val type = ParticleTypes.STREAM_CODEC.decode(buf) + PacketParticleS2C(type, pos, velocity) + }) + + } +} \ No newline at end of file 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..e065d66d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt @@ -0,0 +1,49 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.particles.control.ControlType +import net.minecraft.network.PacketByteBuf + +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleStyleS2C( + val uuid: UUID, + val type: ControlType, + val args: Map> +) { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_style") + val payloadID = ResourceLocation(identifierID) + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + buf.writeInt(packet.type.id) + buf.writeInt(packet.args.size) + packet.args.forEach { (t, u) -> + val encode = 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 = ControlType.getTypeById(id) + val argsCount = buf.readInt() + repeat(argsCount) { + val len = buf.readInt() + val key = buf.readUtf() + val buf = buf.readBytes(len) + val decode = ParticleControlerDataBuffers.decodeToBuffer(buf) + args[key] = decode + } + return PacketParticleStyleS2C( + uuid, type, args + ) + } + ) + + } \ No newline at end of file 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..ecaa0468 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt @@ -0,0 +1,50 @@ +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(identifierID) + + @JvmStatic + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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) + val packet = PacketRenderEntityS2C(uuid, bytes, id, Method.idOf(method)) + return packet + } + ) + } \ No newline at end of file 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..45e6d8ac --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt @@ -0,0 +1,86 @@ +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 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(identifierID) + + @JvmStatic + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, packet -> + 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..05cb86d4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt @@ -0,0 +1,234 @@ +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(identifierID) + + private val emptySound = ResourceLocation.withDefaultNamespace("empty") + + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = 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 + } + } \ No newline at end of file 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..85f234b9 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt @@ -0,0 +1,74 @@ +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(identifierID) + + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = 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 + ) + } + } \ No newline at end of file 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..fb0d5831 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt @@ -0,0 +1,440 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +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 + +/** + * 从服务端向客户端同步一个地形效果组的替换或增量更新。 + * + * 协议支持完整替换、追加位置、移除位置、替换 uniform 和删除整组。位置使用相对首坐标的 ZigZag + * 差值编码,生效时间使用相对首个绝对 tick 的差值编码,避免大批连续方块重复传输完整坐标和时间。 + * 包实例只由 companion object 中的工厂创建,并由 + * [cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectManager] 发送。 + */ +@CooAutoRegister +class PacketTerrainEffectGroupS2C() : CooPacket() { + /** 当前包的协议操作类型。 */ + var operation: Int = REPLACE + + /** 目标效果组所属的维度 ID。 */ + var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") + + /** 目标效果组 ID。 */ + var groupId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + + /** 完整替换操作引用的方块 Pipeline ID。 */ + var pipelineId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + + /** 完整替换操作中组开始的绝对游戏 tick。 */ + var startedAt: Long = 0L + + /** 完整替换操作中组到期的绝对游戏 tick;`null` 表示持久组。 */ + var expiresAt: Long? = null + + /** 完整替换操作中用于重叠组排序的服务端序号。 */ + var sequence: Long = 0L + + /** 用于丢弃重复或乱序更新的单调协议版本号。 */ + var revision: Long = 0L + + /** 完整替换和 options 更新中的有符号绘制优先级。 */ + var priority: Int = 0 + + /** 完整替换和 options 更新中的合成方式。 */ + var composition: CooTerrainEffectComposition = CooTerrainEffectComposition.REPLACE + + /** + * 操作涉及的位置数据。 + * + * 完整替换和追加操作中,键为方块坐标,值为绝对生效 tick;移除位置操作只使用键。 + */ + var positions: Map = emptyMap() + + /** 键为 shader uniform 名称,值为完整替换或 uniform 更新携带的数据。 */ + var uniforms: Map = emptyMap() + + /** @return 地形效果组服务端到客户端协议的固定资源 ID。 */ + override fun id(): ResourceLocation = ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, + PACKET_ID + ) + + /** @return 编解码本协议全部操作类型的 CommonCodec。 */ + override fun codec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = CODEC + + /** + * 在客户端主线程把本包更新应用到 [CooTerrainEffectRegistry]。 + * + * @param context 当前客户端网络处理上下文 + */ + override fun onClientReceive(context: 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 + ) + } + } + } + + /** 创建各协议操作并实现紧凑的坐标、时间和 uniform 编解码。 */ + 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 + + private val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of(::encode, ::decode) + + /** + * 创建完整替换效果组的协议包。 + * + * @param snapshot 要同步的完整服务端效果组快照 + * @return 携带 Pipeline、时间、位置和 uniform 的替换包 + */ + 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 + } + } + + /** + * 创建只追加位置和绝对生效时间的协议包。 + * + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @param positions 键为新增方块坐标,值为绝对生效 tick + * @return 位置追加包 + */ + 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 + } + } + + /** + * 创建删除整个效果组的协议包。 + * + * @param dimension 组所属维度 ID + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @return 整组删除包 + */ + 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 + } + } + + /** + * 创建只删除组内指定位置的协议包。 + * + * @param dimension 组所属维度 ID + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @param positions 要删除的方块坐标 + * @return 位置删除包 + */ + 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 } + } + } + + /** + * 创建替换整组 uniform 集合的协议包。 + * + * @param dimension 组所属维度 ID + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @param uniforms 键为 shader uniform 名称,值为新的完整 uniform 数据 + * @return uniform 更新包 + */ + 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 + } + + /** + * 以首项为坐标和时间基准写入一组位置。 + * + * @param buffer 目标网络缓冲 + * @param positions 键为方块坐标,值为该位置的绝对生效 tick + */ + 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)) + } + } + + /** + * 读取以首项为基准编码的位置和绝对生效时间。 + * + * @param buffer 源网络缓冲 + * @return 键为方块坐标、值为绝对生效 tick 的映射 + */ + 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)) + } + } + + /** @return 从相对坐标差值还原出的方块位置集合。 */ + 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()) + ) + ) + } + } + } + + /** + * 写入带类型标记的 uniform 映射。 + * + * @param buffer 目标网络缓冲 + * @param uniforms 键为 uniform 名称,值为浮点数、整数或向量数据 + */ + private fun writeUniforms(buffer: PacketByteBuf, uniforms: Map) { + buffer.writeVarInt(uniforms.size) + uniforms.forEach { (name, value) -> + buffer.writeUtf(name) + CooUniformValue.STREAM_CODEC.encode(buffer, value) + } + } + + /** + * 读取带类型标记的 uniform 映射。 + * + * @param buffer 源网络缓冲 + * @return 键为 uniform 名称、值为对应类型数据的映射 + */ + 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..7c95aa59 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt @@ -0,0 +1,186 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +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 + +/** + * 程序化 terrain mapping 的服务端到客户端同步包。 + * + * 包只携带区域参数和实例参数,不携带 BlockPos、非空气方块或高度图结果。 + */ +@CooAutoRegister +class PacketTerrainMappingS2C() : CooPacket() { + 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(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = CODEC + + override fun onClientReceive(context: 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 + private val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.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/packet/server/listener/ServerKeyActionHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt new file mode 100644 index 00000000..d010466c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt @@ -0,0 +1,17 @@ +package cn.coostack.cooparticlesapi.network.packet.server.listener + +import cn.coostack.cooparticlesapi.event.CooEventBus +import cn.coostack.cooparticlesapi.event.events.key.KeyActionEvent +import cn.coostack.cooparticlesapi.network.packet.client.PacketKeyActionC2S +import cn.coostack.cooparticlesapi.platform.network.ServerContext + +object ServerKeyActionHandler { + fun receive(packet: PacketKeyActionC2S, context: ServerContext) { + val player = context.player() + context.server().execute { + CooEventBus.call( + KeyActionEvent(player, packet.keyActions, true) + ) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt new file mode 100644 index 00000000..bc7b67d0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt @@ -0,0 +1,38 @@ +package cn.coostack.cooparticlesapi.network.packet.status + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.performance.PerformanceStatusClientBridge +import cn.coostack.cooparticlesapi.performance.PerformanceStatusControlAction +import net.minecraft.resources.ResourceLocation + +/** 服务端命令向目标玩家客户端发送的 Status 会话控制包。 */ +@CooAutoRegister +class PacketPerformanceStatusControlS2C() : CooPacket() { + /** 网络动作名称。 */ + @CodecField var action: String = "" + + /** 使用类型安全动作创建控制包。 */ + constructor(action: PerformanceStatusControlAction) : this() { + this.action = action.name + } + + /** 返回该业务包的稳定协议 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** 经纯 common bridge 把动作交给客户端初始化时安装的控制器。 */ + override fun onClientReceive(context: ClientContext) { + PerformanceStatusControlAction.fromNetworkName(action)?.let(PerformanceStatusClientBridge::handle) + } + + /** 协议注册常量。 */ + companion object { + /** 业务包稳定注册 ID,属于网络协议的一部分。 */ + private const val PACKET_ID = "performance_status_control_s2c" + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt new file mode 100644 index 00000000..feb102c0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt @@ -0,0 +1,39 @@ +package cn.coostack.cooparticlesapi.network.packet.status + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.CooPacketKind +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerRequestGate +import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshotFactory +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.resources.ResourceLocation + +/** 客户端在 Status 活跃期间按配置间隔发送的无参数服务端快照请求。 */ +@CooAutoRegister +class PacketPerformanceStatusRequestC2S : CooPacket() { + /** 返回该业务包的稳定协议 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** 验证请求类型、命令权限和服务端限流后构造固定大小快照。 */ + override fun onServerReceive(context: ServerContext) { + if (context.kind != CooPacketKind.REQUEST) return + val refreshInterval = CooParticlesServices.API_CONFIG_MANAGER.getConfig() + .statusServerRefreshIntervalTicks + if (!PerformanceStatusServerRequestGate.tryAcquire(context.sender, refreshInterval)) return + context.reply( + PacketPerformanceStatusSnapshotS2C.from( + PerformanceStatusServerSnapshotFactory.create(context.sender.server) + ) + ) + } + + /** 协议注册常量。 */ + companion object { + /** 业务包稳定注册 ID,属于网络协议的一部分。 */ + private const val PACKET_ID = "performance_status_request_c2s" + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt new file mode 100644 index 00000000..7094ae10 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt @@ -0,0 +1,191 @@ +package cn.coostack.cooparticlesapi.network.packet.status + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkTotals +import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshot +import cn.coostack.cooparticlesapi.performance.PerformanceStatusVanillaPacketTotals +import net.minecraft.resources.ResourceLocation + +/** 服务端返回给单个查看者的固定字段 Status 快照。 */ +@CooAutoRegister +class PacketPerformanceStatusSnapshotS2C : CooPacket() { + /** 服务端平均 MSPT。 */ + @CodecField var averageMspt: Double = 0.0 + + /** 服务端 Barrage 数。 */ + @CodecField var barrages: Int = 0 + + /** 服务端生成快照的 epoch 毫秒。 */ + @CodecField var capturedAtEpochMillis: Long = 0L + + /** 服务端 CooFX scene 数。 */ + @CodecField var cooFxScenes: Int = 0 + + /** 服务端 Composition 数。 */ + @CodecField var compositions: Int = 0 + + /** 服务端 DisplayEntity 数。 */ + @CodecField var displayEntities: Int = 0 + + /** 服务端 Emitter 数。 */ + @CodecField var emitters: Int = 0 + + /** 服务端 JVM 累计垃圾收集次数。 */ + @CodecField var gcCollectionCount: Long = 0L + + /** 服务端 JVM 累计垃圾收集耗时毫秒。 */ + @CodecField var gcCollectionTimeMs: Long = 0L + + /** 服务端最大堆字节数。 */ + @CodecField var heapMaxBytes: Long = 0L + + /** 服务端已使用堆字节数。 */ + @CodecField var heapUsedBytes: Long = 0L + + /** 服务端历史最大 MSPT。 */ + @CodecField var maxMspt: Double = 0.0 + + /** 服务端累计收到的 CooPacket 业务字节数。 */ + @CodecField var networkReceivedBytes: Long = 0L + + /** 服务端累计收到的 CooPacket 数。 */ + @CodecField var networkReceivedPackets: Long = 0L + + /** 服务端累计发送的 CooPacket 业务字节数。 */ + @CodecField var networkSentBytes: Long = 0L + + /** 服务端累计发送的 CooPacket 数。 */ + @CodecField var networkSentPackets: Long = 0L + + /** 当前在线玩家数。 */ + @CodecField var onlinePlayers: Int = 0 + + /** 服务端历史 P95 MSPT。 */ + @CodecField var p95Mspt: Double = 0.0 + + /** 服务端旧 ParticleGroup 数。 */ + @CodecField var particleGroups: Int = 0 + + /** 服务端配置的 Status 快照刷新间隔。 */ + @CodecField var refreshIntervalTicks: Int = 20 + + /** 服务端 RenderEntity 数。 */ + @CodecField var renderEntities: Int = 0 + + /** 服务端当前 tick 序号。 */ + @CodecField var serverTick: Long = 0L + + /** 服务端管理的声音实例数。 */ + @CodecField var soundInstances: Int = 0 + + /** 服务端管理的循环声音数。 */ + @CodecField var soundLoops: Int = 0 + + /** 当前目标 TPS。 */ + @CodecField var targetTps: Double = 20.0 + + /** 服务端 Terrain effect group 数。 */ + @CodecField var terrainEffectGroups: Int = 0 + + /** 服务端 Terrain mapping 实例数。 */ + @CodecField var terrainMappings: Int = 0 + + /** 当前可维持 TPS。 */ + @CodecField var tps: Double = 20.0 + + /** 服务端原版 Connection 累计收到的 Packet 数。 */ + @CodecField var vanillaNetworkReceivedPackets: Long = 0L + + /** 服务端原版 Connection 累计发送的 Packet 数。 */ + @CodecField var vanillaNetworkSentPackets: Long = 0L + + /** 返回该业务包的稳定协议 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** 把协议字段恢复为不可变服务端快照。 */ + fun toSnapshot(): PerformanceStatusServerSnapshot { + return PerformanceStatusServerSnapshot( + capturedAtEpochMillis = capturedAtEpochMillis, + serverTick = serverTick, + refreshIntervalTicks = refreshIntervalTicks.coerceIn(5, 1_200), + targetTps = targetTps, + tps = tps, + averageMspt = averageMspt, + p95Mspt = p95Mspt, + maxMspt = maxMspt, + onlinePlayers = onlinePlayers, + particleGroups = particleGroups, + renderEntities = renderEntities, + displayEntities = displayEntities, + emitters = emitters, + compositions = compositions, + gcCollectionCount = gcCollectionCount, + gcCollectionTimeMs = gcCollectionTimeMs, + terrainEffectGroups = terrainEffectGroups, + terrainMappings = terrainMappings, + soundInstances = soundInstances, + soundLoops = soundLoops, + barrages = barrages, + cooFxScenes = cooFxScenes, + heapUsedBytes = heapUsedBytes, + heapMaxBytes = heapMaxBytes, + cooPackets = PerformanceStatusNetworkTotals( + sentPackets = networkSentPackets, + sentBytes = networkSentBytes, + receivedPackets = networkReceivedPackets, + receivedBytes = networkReceivedBytes, + ), + vanillaPackets = PerformanceStatusVanillaPacketTotals( + sentPackets = vanillaNetworkSentPackets, + receivedPackets = vanillaNetworkReceivedPackets, + ), + ) + } + + /** 协议注册常量和快照转换入口。 */ + companion object { + /** 业务包稳定注册 ID,属于网络协议的一部分。 */ + private const val PACKET_ID = "performance_status_snapshot_s2c" + + /** 从不可变服务端快照创建可反射编码的业务包。 */ + fun from(snapshot: PerformanceStatusServerSnapshot): PacketPerformanceStatusSnapshotS2C { + return PacketPerformanceStatusSnapshotS2C().also { packet -> + packet.averageMspt = snapshot.averageMspt + packet.barrages = snapshot.barrages + packet.capturedAtEpochMillis = snapshot.capturedAtEpochMillis + packet.cooFxScenes = snapshot.cooFxScenes + packet.compositions = snapshot.compositions + packet.displayEntities = snapshot.displayEntities + packet.emitters = snapshot.emitters + packet.gcCollectionCount = snapshot.gcCollectionCount + packet.gcCollectionTimeMs = snapshot.gcCollectionTimeMs + packet.heapMaxBytes = snapshot.heapMaxBytes + packet.heapUsedBytes = snapshot.heapUsedBytes + packet.maxMspt = snapshot.maxMspt + packet.networkReceivedBytes = snapshot.cooPackets.receivedBytes + packet.networkReceivedPackets = snapshot.cooPackets.receivedPackets + packet.networkSentBytes = snapshot.cooPackets.sentBytes + packet.networkSentPackets = snapshot.cooPackets.sentPackets + packet.onlinePlayers = snapshot.onlinePlayers + packet.p95Mspt = snapshot.p95Mspt + packet.particleGroups = snapshot.particleGroups + packet.refreshIntervalTicks = snapshot.refreshIntervalTicks + packet.renderEntities = snapshot.renderEntities + packet.serverTick = snapshot.serverTick + packet.soundInstances = snapshot.soundInstances + packet.soundLoops = snapshot.soundLoops + packet.targetTps = snapshot.targetTps + packet.terrainEffectGroups = snapshot.terrainEffectGroups + packet.terrainMappings = snapshot.terrainMappings + packet.tps = snapshot.tps + packet.vanillaNetworkReceivedPackets = snapshot.vanillaPackets.receivedPackets + packet.vanillaNetworkSentPackets = snapshot.vanillaPackets.sentPackets + } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt new file mode 100644 index 00000000..ba8c6d2c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt @@ -0,0 +1,48 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.BoundControllerEntry +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketOpenBoundTestSelectionScreenS2C() : CooPacket() { + @CodecField var currentIndices: List = emptyList() + @CodecField var currentOptionIds: List = emptyList() + @CodecField var dimensions: List = emptyList() + @CodecField var groupIds: List = emptyList() + @CodecField var loaded: List = emptyList() + @CodecField var optionCounts: List = emptyList() + @CodecField var positions: List = emptyList() + @CodecField var running: List = emptyList() + @CodecField var statuses: List = emptyList() + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_bound_test_selection_screen_s2c") + } + + override fun onClientReceive(context: ClientContext) { + TestControllerClientScreens.openBoundSelection(this) + } + + companion object { + fun fromEntries(entries: List): PacketOpenBoundTestSelectionScreenS2C { + return PacketOpenBoundTestSelectionScreenS2C().also { + it.dimensions = entries.map { entry -> entry.dimension } + it.positions = entries.map { entry -> entry.pos } + it.groupIds = entries.map { entry -> entry.groupId } + it.statuses = entries.map { entry -> entry.status } + it.currentIndices = entries.map { entry -> entry.currentIndex } + it.currentOptionIds = entries.map { entry -> entry.currentOptionId } + it.optionCounts = entries.map { entry -> entry.optionCount } + it.loaded = entries.map { entry -> entry.loaded } + it.running = entries.map { entry -> entry.running } + } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt new file mode 100644 index 00000000..3139e246 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt @@ -0,0 +1,123 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockEntity +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens +import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec +import cn.coostack.cooparticlesapi.test.api.TestOptionParamSpec +import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketOpenTestControllerScreenS2C() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var boxDepth: Double = 0.6 + @CodecField var boxHeight: Double = 1.8 + @CodecField var boxWidth: Double = 0.6 + @CodecField var currentIndex: Int = 0 + @CodecField var dimension: String = "" + @CodecField var forwardX: Double = 0.0 + @CodecField var forwardY: Double = 0.0 + @CodecField var forwardZ: Double = 1.0 + @CodecField var forwardDynamic: Boolean = false + @CodecField var forwardTrack: String = "" + @CodecField var groupId: String = "" + @CodecField var mode: String = "sequential" + @CodecField var offsetX: Double = 0.0 + @CodecField var offsetY: Double = 0.0 + @CodecField var offsetZ: Double = 0.0 + @CodecField var optionCount: Int = 0 + @CodecField var optionIds: List = emptyList() + @CodecField var optionParamSpecs: List = emptyList() + @CodecField var optionParamValues: List = emptyList() + @CodecField var pendingReview: Boolean = false + @CodecField var positionDynamic: Boolean = false + @CodecField var positionTrack: String = "" + @CodecField var registeredIds: List = emptyList() + @CodecField var registeredOptionIds: List = emptyList() + @CodecField var registeredOptionParamSpecs: List> = emptyList() + @CodecField var repeatDelayTicks: Int = 0 + @CodecField var repeatIndex: Boolean = false + @CodecField var running: Boolean = false + @CodecField var selectedIndex: Int = 0 + @CodecField var status: String = "" + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_test_controller_screen_s2c") + } + + override fun onClientReceive(context: ClientContext) { + TestControllerClientScreens.openController(this) + } + + companion object { + /** + * 从服务端方块实体创建控制界面快照。 + * + * @param dimension 方块所在维度的资源 ID 文本 + * @param blockEntity 待同步的测试控制器方块实体 + * @param registeredIds 当前可用的方块测试组资源 ID + * @return 可发送到客户端的完整界面数据包 + */ + fun fromBlockEntity( + dimension: String, + blockEntity: TestControllerBlockEntity, + registeredIds: List + ): PacketOpenTestControllerScreenS2C { + return PacketOpenTestControllerScreenS2C().also { + it.dimension = dimension + it.blockPos = blockEntity.blockPos + it.groupId = blockEntity.groupId + it.mode = blockEntity.mode.id + it.selectedIndex = blockEntity.selectedIndex + it.repeatIndex = blockEntity.repeatIndex + it.repeatDelayTicks = blockEntity.repeatDelayTicks + it.offsetX = blockEntity.playerOffset.x + it.offsetY = blockEntity.playerOffset.y + it.offsetZ = blockEntity.playerOffset.z + it.forwardX = blockEntity.playerForward.x + it.forwardY = blockEntity.playerForward.y + it.forwardZ = blockEntity.playerForward.z + it.forwardDynamic = blockEntity.playerForwardDynamic + it.forwardTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerForwardTrack) + it.positionDynamic = blockEntity.playerPositionDynamic + it.positionTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerPositionTrack) + it.boxWidth = blockEntity.playerBoxWidth + it.boxHeight = blockEntity.playerBoxHeight + it.boxDepth = blockEntity.playerBoxDepth + it.running = blockEntity.isRunning() + it.status = blockEntity.statusText() + it.optionCount = blockEntity.optionCount() + it.optionIds = ArrayList(blockEntity.optionIds()) + it.optionParamSpecs = encodeOptionParamSpecs(blockEntity.optionParamSpecs()) + it.optionParamValues = TestOptionParamCodec.encodeAllOptionValues(blockEntity.optionParamValues()) + it.pendingReview = blockEntity.hasPendingReview() + it.currentIndex = blockEntity.currentIndex() + it.registeredIds = registeredIds.map(ResourceLocation::toString) + it.registeredOptionIds = ArrayList(registeredIds.map { id -> + encodeOptionIds(blockEntity.optionIds(id)) + }) + it.registeredOptionParamSpecs = ArrayList(registeredIds.map { id -> + encodeOptionParamSpecs(blockEntity.optionParamSpecs(id)) + }) + } + } + + private fun encodeOptionIds(optionIds: List): String { + return optionIds.joinToString(OPTION_ID_SEPARATOR) + } + + private fun encodeOptionParamSpecs(optionSpecs: List>>): List { + return TestOptionParamCodec.encodeAllOptionSpecs(optionSpecs).map { encoded -> + encoded.encodeToByteArray() + } + } + + const val OPTION_ID_SEPARATOR: String = "\u001F" + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt new file mode 100644 index 00000000..3d3ac4b0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import cn.coostack.cooparticlesapi.test.block.BlockTestOptionResult +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketReviewTestControllerC2S() : CooPacket() { + @CodecField var action: String = "" + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos, action: String) : this() { + this.dimension = dimension + this.blockPos = blockPos + this.action = action + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "review_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + val result = when { + action.equals(PASS, ignoreCase = true) -> BlockTestOptionResult.PASSED + action.equals(FAIL, ignoreCase = true) -> BlockTestOptionResult.FAILED + action.equals(SKIP, ignoreCase = true) -> BlockTestOptionResult.SKIPPED + else -> return + } + TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.reviewCurrent(result) + } + + companion object { + const val PASS = "pass" + const val FAIL = "fail" + const val SKIP = "skip" + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt new file mode 100644 index 00000000..39c2c610 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketSelectBoundTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos) : this() { + this.dimension = dimension + this.blockPos = blockPos + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "select_bound_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return + TestControllerBlockAccess.openControllerScreen(sender, blockEntity) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt new file mode 100644 index 00000000..39a58572 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketStartTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos) : this() { + this.dimension = dimension + this.blockPos = blockPos + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "start_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.startTest() + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt new file mode 100644 index 00000000..a0389b03 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketStopTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos) : this() { + this.dimension = dimension + this.blockPos = blockPos + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "stop_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.stopTest() + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt new file mode 100644 index 00000000..5fe02c17 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt @@ -0,0 +1,80 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec +import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec +import cn.coostack.cooparticlesapi.test.block.BlockTestMode +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +@CooAutoRegister +class PacketUpdateTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var boxDepth: Double = 0.6 + @CodecField var boxHeight: Double = 1.8 + @CodecField var boxWidth: Double = 0.6 + @CodecField var dimension: String = "" + @CodecField var forwardX: Double = 0.0 + @CodecField var forwardY: Double = 0.0 + @CodecField var forwardZ: Double = 1.0 + @CodecField var forwardDynamic: Boolean = false + @CodecField var forwardTrack: String = "" + @CodecField var groupId: String = "" + @CodecField var mode: String = "sequential" + @CodecField var offsetX: Double = 0.0 + @CodecField var offsetY: Double = 0.0 + @CodecField var offsetZ: Double = 0.0 + @CodecField var optionParamIndex: Int = 0 + @CodecField var optionParamValues: String = "" + @CodecField var positionDynamic: Boolean = false + @CodecField var positionTrack: String = "" + @CodecField var repeatDelayTicks: Int = 0 + @CodecField var repeatIndex: Boolean = false + @CodecField var reopen: Boolean = false + @CodecField var selectedIndex: Int = 0 + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "update_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return + val wasRunning = blockEntity.isRunning() + // 待人工复核时重启会丢弃尚未给出的复核结果,交由随后的复核包决定测试组去向。 + val pendingReview = blockEntity.hasPendingReview() + val offset = Vec3(offsetX, offsetY, offsetZ) + val forward = Vec3(forwardX, forwardY, forwardZ) + val changed = blockEntity.updateConfig( + groupId = groupId, + mode = BlockTestMode.fromId(mode), + selectedIndex = selectedIndex, + repeatIndex = repeatIndex, + repeatDelayTicks = repeatDelayTicks, + playerOffset = offset, + playerForward = forward, + playerPositionDynamic = positionDynamic, + playerForwardDynamic = forwardDynamic, + playerPositionTrack = BlockTestAnimationTrackCodec.decode(positionTrack, offset), + playerForwardTrack = BlockTestAnimationTrackCodec.decode(forwardTrack, forward), + playerBoxWidth = boxWidth, + playerBoxHeight = boxHeight, + playerBoxDepth = boxDepth, + optionParamIndex = optionParamIndex, + optionParamValues = TestOptionParamCodec.decodeOptionValues(optionParamValues) + ) + if (wasRunning && changed && !pendingReview) { + blockEntity.startTest() + } + if (reopen) { + TestControllerBlockAccess.openControllerScreen(sender, blockEntity) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt new file mode 100644 index 00000000..2f0ae949 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt @@ -0,0 +1,164 @@ +package cn.coostack.cooparticlesapi.network.particle + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.utils.MathDataUtil + +/** + * 由于服务端获取不到客户层的粒子总数 + * 所以这里不做粒子上限 + * 但是记录是用于 其他客户端同步 + * + * 如果要设定自动同步 + * 在 otherPacketArgs 设置 "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount) + * 或者 在这个方法中 return mapOf(toggleArgLeastIndex(),toggleArgStatus(),...其他自定义的数据) + * + * 你无需在 SequencedServerParticleGroup的Provider中 手动应用toggle输入的count + * 服务器会自动处理此参数 + * + * 数据包设定 + * addCount args type int -> count + * removeCount args type int -> count + * toggle args type int-> count + */ +@Deprecated("使用ParticleComposition") +abstract class SequencedServerParticleGroup(visibleRange: Double = 32.0) : ServerParticleGroup(visibleRange) { + /** + * 记录了每一个粒子的显示状态 (只要maxCount确实等于loadParticleLocationsWithIndex返回的map的size) + */ + private val clientIndexStatus = LongArray((maxCount() / 64) + 1) + + var serverSequencedParticleCount = 0 + + /** + * 切记一定要和 SequencedParticleGroup.loadParticleLocationsWithIndex().size 相同 + * + * 如果你的group的粒子数量是可变的(使用了flush方法刷新了粒子样式 其中长度发生变化) + * + * 那么请在服务器层做好数据同步 ( size同步 ) + * + * 如果此处的 maxCount > SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致数组越界异常 + * + * 如果此处的 maxCount < SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致粒子控制不完全(部分粒子无法从服务器生成) + * + * @return 应当返回 SequencedParticleGroup.loadParticleLocationsWithIndex().size + */ + abstract fun maxCount(): Int + + fun isDisplayed(index: Int): Boolean { + val storage = MathDataUtil.getStoragePageLong(index) + if (storage >= clientIndexStatus.size) { + return false + } + + return MathDataUtil.getStorageWithBitLong( + MathDataUtil.getStorageWithBitLong(index) + ) == 1 + } + + fun setDisplayed(index: Int, status: Boolean) { + val storage = MathDataUtil.getStorageWithBitLong(index) + val page = MathDataUtil.getStoragePageLong(index) + if (page >= clientIndexStatus.size) { + return + } + clientIndexStatus[page] = MathDataUtil.setStatusLong(clientIndexStatus[page], storage, status) + } + + fun toggleArgLeastIndex(): Pair> { + return "toggle" to ParticleControlerDataBuffers.int(1) + } + + fun toggleArgStatus(): Pair> { + return "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) + } + + fun addSingle() { + if (serverSequencedParticleCount >= maxCount()) { + return + } + change( + { + setDisplayed(serverSequencedParticleCount++, true) + }, + mapOf( + "addCount" to ParticleControlerDataBuffers.int(1) + ) + ) + } + + fun addMultiple(count: Int) { + change( + { + for (index in serverSequencedParticleCount until serverSequencedParticleCount + count) { + setDisplayed(index, true) + } + serverSequencedParticleCount += count + }, + mapOf( + "addCount" to ParticleControlerDataBuffers.int(count) + ) + ) + if (serverSequencedParticleCount >= maxCount()) { + serverSequencedParticleCount = maxCount() + } + } + + fun removeSingle(): Boolean { + if (serverSequencedParticleCount <= 0) { + return false + } + change( + { + setDisplayed(serverSequencedParticleCount--, false) + }, + mapOf( + "removeCount" to ParticleControlerDataBuffers.int(1) + ) + ) + return true + } + + fun removeMultiple(count: Int) { + if (serverSequencedParticleCount <= 0) { + return + } + change( + { + for (index in serverSequencedParticleCount - count + 1..serverSequencedParticleCount) { + setDisplayed(index, false) + } + serverSequencedParticleCount -= count + if (serverSequencedParticleCount <= 0) { + serverSequencedParticleCount = 0 + } + }, + mapOf( + "removeCount" to ParticleControlerDataBuffers.int(count) + ) + ) + } + + fun changeSingle(index: Int, status: Boolean) { + if (index >= maxCount() || index < 0) { + return + } + change( + { + setDisplayed(index, status) + }, mapOf( + "change_single" to ParticleControlerDataBuffers.int(index) + ) + ) + } + + fun toggleCurrentCount() { + change( + {}, + mapOf( + "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount), + "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) + ) + ) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt new file mode 100644 index 00000000..f515823d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt @@ -0,0 +1,328 @@ +package cn.coostack.cooparticlesapi.network.particle + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.api.controler.server.ServerControler +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +/** + * @param clientGroup 客户端渲染的类型 + * @param uuid 粒子组合唯一标识符 + * @param visibleRange 玩家可见范围 (origin) + */ +@Deprecated("使用ParticleComposition") + +abstract class ServerParticleGroup( + var visibleRange: Double = 32.0 +) : ServerControler { + val uuid: UUID = UUID.randomUUID() + var pos: Vec3 = Vec3.ZERO + internal set + var world: Level? = null + internal set + internal var valid = true + var canceled = false + internal set + + /** + * 客户端渲染视角下的生命周期 + */ + var clientTick = 0 + internal set + var clientMaxTick = 120 + internal set + var scale = 1.0 + + /** + * 服务器内的生命周期 + * 同步数据时不会传输给客户端 + */ + var tick = 0 + var maxTick = 120 + + + var axis: RelativeLocation = RelativeLocation(0.0, 1.0, 0.0) + internal set + + /** + * 每个tick能做的事情 + * 例如绑定实体位置 旋转等 + */ + abstract fun tick() + + /** + * 获取自定义包的其他参数 (用于服务器传输给客户端) + */ + abstract fun otherPacketArgs(): Map> + + /** + * 获取对应的客户端层Class + * @return null 不能单独生成的类型 + */ + abstract fun getClientType(): Class? + + + /** + * 当粒子组合在服务器创建并且发送到客户端时执行 + * 可以给客户端发送一些包修改 + */ + open fun onGroupDisplay(pos: Vec3, world: ServerLevel) {} + + /** + * 通过doTickAlive函数导致粒子组合到达最终生命周期时执行的函数 + * 不强制重写 + */ + open fun onTickAliveDeath() {} + + /** + * 同步后客户端层面粒子死亡 (除非没开启tickAlive) + * 服务器处理上同样 + */ + open fun onClientViewDeath() {} + + /** + * 可以在abstract tick中执行 + */ + fun doTickClient() { + if (clientTick++ >= clientMaxTick) { + onClientViewDeath() + } + } + + /** + * 可以在abstract tick中执行 + */ + fun doTickAlive() { + if (tick++ >= maxTick) { + kill() + onTickAliveDeath() + } + } + + /** + * 不立刻同步给客户端 + * 只修改参数 + * 但是在新加入可视的玩家会应用此scale + * 如果想要在客户端不应用服务器的scale + * 则需要在provider的create方法中重新修改scale值 + */ + fun scaleOnServer(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale must be greater than zero.") + return + } + this.scale = new + } + + /** + * 服务器修改客户端缩放大小 + * 并且立刻同步到客户端 + */ + fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale must be greater than zero.") + return + } + change( + { + this.scale = new + }, + mapOf( + PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(new) + ) + ) + } + + /** + * 销毁包 (在update时会删除所有的可见) + */ + fun kill() { + canceled = true + world ?: return + val visible = ServerParticleGroupManager.filterVisiblePlayer(this) + visible.forEach { + val player = world!!.getPlayerByUUID(it) ?: return@forEach + // 发销毁包 + val packet = PacketParticleGroupS2C(uuid, ControlType.REMOVE, mapOf()) + CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) + } + } + + /** + * 跟随玩家在线状态. 世界跟换 删除粒子 + */ + fun withPlayerStats(player: ServerPlayer) { + if (player.hasDisconnected()) { + kill() + } + + if (player.level() != world) { + kill() + } + } + + fun withEntityStats(entity: LivingEntity) { + if (entity.isDeadOrDying) { + kill() + } + + if (entity.level() != world) { + kill() + } + + } + + fun setAxis(axis: Vec3) { + change( + { this.axis = RelativeLocation.of(axis) }, mapOf( + PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(axis) + ) + ) + } + + /** + * 不发送到客户端 + */ + fun setPosOnServer(pos: Vec3) { + this.pos = pos + } + + /** + * 同步旋转后的中心点和对称轴 + */ + fun setRotateToOnServer(to: RelativeLocation) { + axis = to.normalize() + } + + + /** + * 出现了一些控制类之外的传送 + */ + fun teleportGroupTo(pos: Vec3) { + // 发包告知 所有可见客户端 + change( + { this.pos = pos }, mapOf( + PacketParticleGroupS2C.PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(pos) + ) + ) + } + + override fun getValue(): ServerParticleGroup { + return this + } + + override fun remove() { + kill() + } + + + override fun teleportTo(to: Vec3) { + teleportGroupTo(to) + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + /** + * 出现了一些控制类之外的旋转 + */ + override fun rotateAsAxis(angle: Double) { + change( + {}, mapOf( + PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( + angle + ) + ) + ) + } + + override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { + change( + {}, mapOf( + PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( + angle + ), + PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d( + to.toVector() + ) + ) + ) + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ServerLevel) return + ServerParticleGroupManager.addParticleGroup(this, pos, world) + } + + /** + * 出现了一些控制类之外的旋转 + */ + override fun rotateToPoint(to: RelativeLocation) { + rotateParticlesToPoint(to.toVector()) + } + + fun rotateParticlesToPoint(to: Vec3) { + change( + {}, mapOf( + PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d(to) + ) + ) + } + + fun changeTick(tick: Int) { + change( + { clientTick = tick }, mapOf( + PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int( + tick + ) + ) + ) + } + + fun changeMaxTick(tick: Int) { + change( + { this.maxTick = tick }, mapOf( + PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(tick) + ) + ) + } + + /** + * 发送行为修改包 + * @param toggleMethod 修改Server层的参数的方法 如果不用此修改方法则会导致其他客户端渲染不同步 + * @param args 此参数出现过的键 必须在otherPacketArgs方法返回的args中存在 否则任然会存在同步问题 + */ + fun change(toggleMethod: ServerParticleGroup.() -> Unit, args: Map>) { + world ?: return + val visible = ServerParticleGroupManager.filterVisiblePlayer(this) + toggleMethod(this) + visible.forEach { + val player = world!!.getPlayerByUUID(it) ?: return@forEach + val packet = PacketParticleGroupS2C(uuid, ControlType.CHANGE, args) + CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) + } + } + + fun initServerGroup(pos: Vec3, world: Level, maxTick: Int = 120) { + this.pos = pos + this.world = world + this.clientMaxTick = maxTick + } + + override fun isValid(): Boolean { + return valid + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt new file mode 100644 index 00000000..1588441b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt @@ -0,0 +1,200 @@ +package cn.coostack.cooparticlesapi.network.particle + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C.PacketArgsType +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.phys.Vec3 +import java.util.* +import java.util.concurrent.ConcurrentHashMap + + +/** + * 控制所有的ServerParticleGroup + */ +@Deprecated("使用ParticleComposition") + +object ServerParticleGroupManager { + private val serverGroups = ConcurrentHashMap() + + /** + * key playerUUID + * value player can see + */ + internal val visible = ConcurrentHashMap>() + + fun addParticleGroup( + group: ServerParticleGroup, + pos: Vec3, + world: ServerLevel + ) { + serverGroups[group.uuid] = group + group.initServerGroup(pos, world) + world.players().filter { it.position().distanceTo(pos) <= group.visibleRange } + .forEach { receiver -> + addGroupPlayerView(receiver, group) + } + group.onGroupDisplay(pos, world) + } + + fun getParticleGroup(group: UUID): ServerParticleGroup? { + return serverGroups[group] + } + + fun getGroups(): Map { + return Collections.unmodifiableMap(serverGroups) + } + + /** 返回服务端当前旧 ParticleGroup 实例数。 */ + fun groupCount(): Int = serverGroups.size + + fun clearServer() { + serverGroups.onEach { it.value.canceled = true }.clear() + visible.clear() + } + + fun upgrade() { + upgradeGroups() + clearOfflineVisible() + } + + fun filterVisiblePlayer(group: ServerParticleGroup): Set { + val set = HashSet() + visible.forEach { + if (group in it.value) { + set.add(it.key) + } + } + return set + } + + private fun upgradeGroups() { + val iterator = serverGroups.iterator() + while (iterator.hasNext()) { + val value = iterator.next().value + // 更新状态 + if (value.canceled || !value.valid) { + val server = value.world?.server ?: continue + visible.forEach { (t, u) -> + val player = server.playerList.getPlayer(t) ?: return@forEach + if (value in u) { + removeGroupPlayerView(player, value) + u.remove(value) + } + } + iterator.remove() + continue + } + // 更新可见性 + value.world!!.server!!.playerList.players.forEach { p -> + val visibleSet = visible.getOrPut(p.uuid) { HashSet() } + if (p.level().dimension() != value.world?.dimension()) { + // 世界转换 + if (value in visibleSet) { + removeGroupPlayerView(p, value) + } + visibleSet!!.remove(value) + return@forEach + } + if (p.isDeadOrDying) { + if (value in visibleSet) { + removeGroupPlayerView(p, value) + visibleSet!!.remove(value) + } + return@forEach + } + if (value.pos.distanceTo(p.position()) <= value.visibleRange) { + // 防止重复添加(发包) + if (value in visibleSet) { + return@forEach + } + addGroupPlayerView(p, value) + // 同步数据包 + togglePacketView(p, value) + + } else { + if (value in visibleSet) { + removeGroupPlayerView(p, value) + } + // 超过范围 + visibleSet.remove(value) + } + } + value.tick() + } + } + + private fun clearOfflineVisible() { + val server = CooParticlesAPI.serverOrNull ?: return + // 清空所有离线玩家 + val visibleIterator = visible.iterator() + while (visibleIterator.hasNext()) { + val entry = visibleIterator.next() + val player = server.playerList.getPlayer(entry.key) + if (player == null || player.hasDisconnected()) { + visibleIterator.remove() + } + } + } + + private fun togglePacketView(target: ServerPlayer, group: ServerParticleGroup) { + val packet = PacketParticleGroupS2C( + group.uuid, ControlType.CHANGE, + mutableMapOf( + PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(group.pos), + PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(group.axis.toVector()), + PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientTick), + PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientMaxTick), + ) + ) + + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + /** + * 在该玩家的视角中移除这些粒子 + * @param target + * @param targetGroup + */ + private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ServerParticleGroup) { + // 发包给玩家 + val packet = PacketParticleGroupS2C( + targetGroup.uuid, + ControlType.REMOVE, mapOf() + ) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + /* + * @param target + * @param targetGroup + */ + private fun addGroupPlayerView( + target: ServerPlayer, + targetGroup: ServerParticleGroup, + ) { + // 修复一个包发送给一个玩家两次 + val visibleSet = visible.getOrPut(target.uuid) { HashSet() } + visibleSet.add(targetGroup) + // 发包给玩家 + val packet = PacketParticleGroupS2C( + targetGroup.uuid, + ControlType.CREATE, + mutableMapOf( + PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(targetGroup.pos), + PacketArgsType.GROUP_TYPE.ofArgs to ParticleControlerDataBuffers.string(targetGroup.getClientType()!!.name), + PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientTick), + PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientMaxTick), + PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(targetGroup.scale) + ).apply { + putAll(targetGroup.otherPacketArgs()) + } + ) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + +} 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..aeff8e9c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt @@ -0,0 +1,18 @@ +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(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return ParticleCompositionRegistryHelper.generateCodec(this) + } +} \ No newline at end of file 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..548bdf6b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt @@ -0,0 +1,16 @@ +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) { + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return ParticleCompositionRegistryHelper.generateCodec(this) + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt new file mode 100644 index 00000000..2b7be814 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt @@ -0,0 +1,51 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.cparticle.CParticle +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.particles.control.ParticleControler +import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect +import java.util.UUID + +class CompositionData : Comparable { + var order = 0 + val uuid = UUID.randomUUID() + val singleParticleHandlers = ArrayList Unit>() + val particleControlerHandlers = ArrayList Unit>() + val cParticleHandlers = ArrayList Unit>() + val cParticleControlerHandlers = ArrayList Unit>() + var displayerBuilder: (UUID) -> ParticleDisplayer = { + ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) + } + private set + + fun setDisplayerSupplier(supplier: (UUID) -> ParticleDisplayer): CompositionData { + displayerBuilder = supplier + return this + } + + fun addParticleInstanceInit(init: ControlableParticle.() -> Unit): CompositionData { + singleParticleHandlers.add(init) + return this + } + + fun addParticleControlerInstanceInit(init: ParticleControler.() -> Unit): CompositionData { + particleControlerHandlers.add(init) + return this + } + + fun addCParticleInstanceInit(init: CParticle.() -> Unit): CompositionData { + cParticleHandlers.add(init) + return this + } + + fun addCParticleControlerInstanceInit(init: CParticleControlable.() -> Unit): CompositionData { + cParticleControlerHandlers.add(init) + return this + } + + override fun compareTo(other: CompositionData): Int { + return order - other.order + } +} 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..aa643918 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt @@ -0,0 +1,1177 @@ +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 + +/** + * 因为不想写太多的ControlableBuffer, 而改用Codec自动生成数据 (同时避免写一万个Provider内部类) + * + * 所以重构ParticleStyle + * + * 首先强制 auto update + * + * 使用自动注册 + * @see cn.coostack.cooparticlesapi.annotations.composition.ParticleCompositionRegister + */ +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 + + /** + * 如果作为子 composition输入就一定要修改这个 + */ + var controlUUID = UUID.randomUUID() + + var axis = RelativeLocation.yAxis() + + var roll = 0.0 + + val particles = ConcurrentHashMap>() + + /** + * 排除掉 SingleParticleDisplayer 从而减少遍历次数 + */ + val controlerTicks = HashSet>() + + val particleLocations = ConcurrentHashMap, RelativeLocation>() + + val status = CompositionStatusHelper() + + /** 当粒子组合初始化时,存储一倍缩放粒子组与原点的距离。 */ + val particleDefaultLength = ConcurrentHashMap() + + /** 保留一倍缩放时的相对向量,使零缩放后的粒子能够恢复方向。 */ + private val particleDefaultLocations = ConcurrentHashMap() + + // 防止频繁的toList造成的性能浪费 + + 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 + + /** + * 标记此 Composition 的完整网络状态需要重新同步。 + * + * 自定义同步字段优先使用 `var value by dirty(initial)`;普通 `@CodecField` 修改后需要显式调用本方法。 + */ + 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(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + + abstract fun getParticles(): Map + + abstract fun onDisplay() + + /** + * 快捷设置 链式调用 + * + * @param interval + * @return + */ + fun setDisabledInterval(interval: Int): ParticleComposition { + this.status.closedInternal = interval + markNetworkStateDirty() + return this + } + + /** + * 配置此 composition 自动创建的所有 CParticle systems。 + * 名称、容量、渲染层、模式和生命周期由 composition 管理。 + */ + fun configureCParticleSystem(configure: CParticleSystem.() -> Unit): ParticleComposition { + return setCParticleSystemConfiguration(null, configure) + } + + /** 只配置指定渲染层的 system。 */ + 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 + } + + /** 返回此 composition 在指定层自动创建的 system;尚未显示时返回 null。 */ + fun getCParticleSystem(layer: CParticleRenderLayer): CParticleSystem? { + return managedCParticleSystems.firstOrNull { !it.released && it.layer == layer } + } + + /** 返回此 composition 当前持有的 systems 快照。 */ + 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 + } + } + + /** + * 对当前 composition 创建的所有 CParticle systems 播放同一段 GPU 视觉过渡。 + * + * 示例:`playCParticleVisualTransition(20F, scaleCurve = curve)` 会统一缩放当前 systems。 + * 禁止:不要用它修改粒子生成时的基础尺寸。 + * + * @param durationTicks 过渡时长,单位 tick + * @param alphaCurve 不透明度倍率曲线 + * @param scaleCurve 等比缩放倍率曲线 + * @param colorFrom 可选起始颜色 + * @param colorTo 可选结束颜色 + * @param mode 过渡结束后的行为 + * @return 当前 composition + */ + @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, + ) + } + + /** + * 播放 composition 级 GPU 视觉过渡,并允许强制重新开始相同配置。 + * + * 示例:`playCParticleVisualTransition(20F, true, scaleCurve = curve)` 会重置进度。 + * 禁止:不要用它修改粒子生成时的基础尺寸。 + * + * @param durationTicks 过渡时长,单位 tick + * @param restart 是否强制从头播放 + * @param alphaCurve 不透明度倍率曲线 + * @param scaleCurve 等比缩放倍率曲线 + * @param colorFrom 可选起始颜色 + * @param colorTo 可选结束颜色 + * @param mode 过渡结束后的行为 + * @return 当前 composition + */ + @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, + ) + } + + /** + * 把同一组过渡参数分发给当前 composition 管理的 systems。 + * + * 示例:公开重载通过本方法统一传递 [restart]。 + * 禁止:不要在这里缓存已经释放的 system。 + * + * @param durationTicks 过渡时长,单位 tick + * @param alphaCurve 不透明度倍率曲线 + * @param scaleCurve 等比缩放倍率曲线 + * @param colorFrom 可选起始颜色 + * @param colorTo 可选结束颜色 + * @param mode 过渡结束后的行为 + * @param restart 是否强制从头播放 + * @return 当前 composition + */ + 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 + } + + /** 停止当前 composition 的 CParticle GPU 视觉过渡。 */ + @JvmOverloads + fun stopCParticleVisualTransition(reset: Boolean = false): ParticleComposition { + getCParticleSystems().forEach { it.stopVisualTransition(reset) } + return this + } + + /** + * 扫描此 composition 的完整运行时子树,为所有 CParticle systems 播放同一段 GPU alpha 过渡。 + * 子树包括嵌套 composition、ParticleGroupStyle 和 ControlableParticleGroup。 + * 过渡曲线会覆盖粒子实例的 alpha,再与粒子、system 和普通视觉过渡的 alpha 曲线相乘。 + * + * @param durationTicks 过渡时长,单位为 tick,必须为有限正数 + * @param alphaCurve 用于覆盖粒子实例 alpha 的曲线 + * @param mode 过渡结束后的行为 + * @param restart 是否强制替换相同配置 + * @return 当前 composition + * @throws IllegalArgumentException 当 [durationTicks] 不是有限正数时抛出 + */ + @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, + ) + } + + /** 停止此 composition 完整运行时子树中的 CParticle alpha 过渡。 */ + @JvmOverloads + 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() + // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 + // 多为服务端(除非有人使在Client环境创建了这个类) + if (displayed) { + if (gpuTransformActive) { + applyGpuScale(new) + } else { + toggleScaleDisplayed() + } + } + // 发包有效 + if (!canceled) { + // remove过后 无法同步 + 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 + } + + /** + * 清除当前显示内容,并按 [cancel] 决定是否结束本次生命周期。 + * + * 示例:`clear(true)` 会销毁子节点并从客户端活动计数中移除本实例。 + * 禁止用 `clear(false)` 表示最终移除,因为该模式用于保留 displayed 状态后重新生成内容。 + * + * @param cancel `true` 表示结束生命周期,`false` 表示仅刷新显示内容 + */ + 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) + } + } + + /** + * 在服务端重新生成 Composition 前重置旧生命周期状态。 + * + * 示例:同一实例再次调用 `spawn(...)` 时先执行本方法,避免沿用 canceled 状态。 + * 禁止在仍需保留当前客户端显示内容时调用。 + */ + internal fun resetLifecycleForSpawn() { + ParticleCompositionManager.setClientLoaded(this, false) + canceled = false + displayed = false + networkStateDirty = true + networkFullDirty = true + lastNetworkStatus = status.displayStatus + lastNetworkStatusInterval = status.closedInternal + } + + /** + * 根据当前世界进入服务端同步或客户端显示生命周期。 + * + * 示例:客户端直接显示的嵌套 Composition 会在此登记为活动实例。 + * 禁止在 [world] 尚未设置时调用。 + */ + 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) + } + } + + /** 按一倍缩放缓存恢复方向后应用当前缩放,避免从零缩放恢复时产生 NaN。 */ + 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 + } + // 减少一倍的new Vec3 + 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 + // display + 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() + } + + /** + * 显示一个相对坐标对应的粒子节点。 + * + * 示例:普通粒子以 [position] 为原点,启用整组变换的 managed CParticle 先计算实际渲染坐标。 + * 禁止把 system 局部坐标直接当作世界坐标,否则光照和位置相关纹理会从错误位置采样。 + * + * @param data 粒子显示方式和初始化动作 + * @param pos 已完成初始缩放与旋转的相对坐标 + */ + 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) { + data.cParticleControlerHandlers.forEach { handler -> + handler(controler) + } + } + registerCParticleNode(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) + } + + /** + * 为未绑定的 CParticle displayer 取得当前 composition 专用的 system。 + * + * 示例:同一 composition、渲染层和纹理绑定会复用同一个 system。 + * 禁止覆盖调用方已经绑定的 system;这种情况返回 `null` 并保留原有行为。 + * + * @param displayer 待绑定的 GPU 粒子显示器 + * @return 本次绑定的 managed system;无法绑定或已有外部绑定时返回 `null` + */ + 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 + } + + /** + * 计算粒子显示与环境采样使用的世界坐标。 + * + * 示例:整组变换启用后,序列后续粒子使用 system 当前矩阵换算真实渲染位置。 + * 禁止在这里返回槽位局部坐标;[CParticleSystem] 会在写入时自行逆变换。 + * + * @param pos composition 中保存的相对坐标 + * @param managedSystem 当前 composition 管理的 GPU system,普通显示器传 `null` + * @return 写入显示器的世界坐标 + */ + 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(), + ) + } + + /** + * 返回整组 GPU 变换启用时使用的稳定槽位坐标。 + * + * 示例:system 原点为 `(10, 20, 30)` 时,相对坐标 `(2, 3, 4)` 写为 `(12, 23, 34)`。 + * 禁止在 CPU 路径提供该坐标;此时 system 需要按现存矩阵逆变换真实世界坐标。 + * + * @param pos composition 保存的相对坐标 + * @param managedSystem 当前 composition 管理的 GPU system + * @return 槽位写入坐标;不使用整组 GPU 变换时返回 `null` + */ + 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: Double) { + val axisVector = normalizedAxis(rotationAxis) + val delta = Matrix4f().rotate(radian.toFloat(), 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..a91ef2fa --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt @@ -0,0 +1,156 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.StatusHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper +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 + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + + /** + * 当反转到0时会自动清理粒子 + */ + 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 + } + + /** + * 改用apply是因为 要防止和addPreTickAction冲突 + * + * @return + */ + 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(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + 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) + } + } + } + } +} \ No newline at end of file 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..62dcde29 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt @@ -0,0 +1,518 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.api.controler.Tickable +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.helper.SequencedCompositionAnimationHelper +import cn.coostack.cooparticlesapi.utils.storage.Memo +import net.minecraft.network.PacketByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.UUID +import kotlin.math.max +import kotlin.math.min + +/** + * 自动更新(AutoToggle) 生长动画 + * + * AI神力 根本懒得再写一份Sequenced的版本了 + */ +abstract 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) + + companion object { + @JvmStatic + fun encodeBase(data: SequencedParticleComposition, buf: PacketByteBuf) { + ParticleComposition.encodeBase(data, buf) + buf.writeInt(data.count) + buf.writeInt(data.displayedParticleCount) + buf.writeInt(data.serverCurrentIndex) + buf.writeLongArray(data.index.get()) + } + + @JvmStatic + fun decodeBase(instance: SequencedParticleComposition, buf: PacketByteBuf) { + ParticleComposition.decodeBase(instance, buf) + instance.count = buf.readInt() + instance.displayedParticleCount = buf.readInt() + instance.serverCurrentIndex = buf.readInt() + instance.index.setMemoValue(buf.readLongArray()) + } + } + + /** + * 这里采用 animate框架 写多个条件用来方便生长动画的条件变化 + * 在init 或者 onDisplay执行 + */ + val animate = + SequencedCompositionAnimationHelper() + .loadComposition(this) + + + var count = 0 + var index = Memo { + var page = count / 64 + if (count % 64 > 0) { + page++ + } + LongArray(page) + } + + /** 服务端:已经“显示”的粒子数量(仅逻辑字段,可用于业务) */ + var displayedParticleCount: Int = 0 + protected set + + /** 服务端:下一个将要生成的 index 指针(用于 addSingle/addMultiple 生长动画) */ + var serverCurrentIndex: Int = 0 + protected set + protected val sequencedParticlesData = ArrayList>() + override fun getParticles(): SortedMap { + return getParticleSequenced() + } + + /** + * 不要调用只有客户端才能调用的方法 因为服务器也会调用一次这个方法用来获取实际的粒子个数 + * + * @return 粒子相对样式 + */ + abstract fun getParticleSequenced(): SortedMap + final override fun tick() { + super.tick() + } + + override fun flush() { + if (particles.isNotEmpty() || indexToUuid.any { it != null }) { + clear(false) + } + displayParticles() + restoreDisplayedParticles() + } + + override fun clear(cancel: Boolean) { + super.clear(cancel) + sequencedParticlesData.clear() + particleRotatedLocations.clear() + indexToUuid.fill(null) + } + + /** + * 进入序列 Composition 的显示生命周期并初始化动画状态。 + * + * 示例:客户端直接显示时会登记为一个活动 Composition 实例。 + * 禁止在 [world] 尚未设置时调用。 + */ + override fun display() { + if (displayed) { + return + } + displayed = true + // 修复禁用后不自动移除的问题 + status.loadControler(this) + status.initHelper() + this.client = world!!.isClientSide + if (client) { + ParticleCompositionManager.setClientLoaded(this, true) + } + // 在服务器需要用来更新粒子个数 所以需要参与一次计算 + flush() + if (!client) { + // 服务器只负责数据同步,不负责粒子生成。 + onDisplay() + return + } + onDisplay() + } + + open fun beforeDisplaySequenced(map: SortedMap) { + + } + + /** + * 这里非常关键: + * - 只计算序列数据、count、缩放/旋转后的相对坐标 + * - 不创建粒子对象(不调用 displayEntry) + */ + override fun displayParticles() { + val locations = getParticles() + val newCount = locations.size + prepareGpuComposition(newCount) + + // 更新粒子总数并扩容位集,同时保留已有状态。 + ensureIndexCapacity(newCount) + + count = newCount + + // 服务端不做任何粒子生成 + if (!client) { + return + } + + beforeDisplaySequenced(locations) + Math3DUtil.rotatePointsToPoint(locations.values.toList(), axis, RelativeLocation.yAxis()) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, roll) + toggleScale(locations) + + sequencedParticlesData.clear() + sequencedParticlesData.addAll(locations.toList()) + particleRotatedLocations.clear() + particleRotatedLocations.addAll(locations.values) + // 客户端:准备索引到 UUID 的映射数组。 + if (indexToUuid.size != count) { + indexToUuid = arrayOfNulls(count) + } + } + + override fun update(other: ParticleComposition) { + super.update(other) + other as SequencedParticleComposition + val oldIndex = this.index.get().copyOf() + val oldCount = this.count + + this.count = other.count + this.displayedParticleCount = other.displayedParticleCount + this.serverCurrentIndex = other.serverCurrentIndex + + this.index.setMemoValue(other.index.get()) + + if (!client) return + + // 粒子总数或缓存结构变化时,客户端需要重新计算序列数据。 + if (oldCount != this.count || sequencedParticlesData.size != this.count) { + // 缓存重建会移除旧节点,flush 会按新的完整位集恢复已显示状态。 + flush() + return + } + + applyIndexDiff(oldIndex, this.index.get()) + restoreDisplayedParticles() + } + + private fun restoreDisplayedParticles() { + if (!client || sequencedParticlesData.size != count) return + for (i in 0 until count) { + if (isParticleDisplayed(i)) createWithIndex(i) + } + } + + private fun applyIndexDiff(oldBits: LongArray, newBits: LongArray) { + + if (!client) return + val n = count + if (n <= 0) return + if (sequencedParticlesData.size != n) return + + val pages = pagesFor(n) + // 复制到同长度,避免越界判断 + val oldSafe = if (oldBits.size == pages) oldBits else oldBits.copyOf(pages) + val newSafe = if (newBits.size == pages) newBits else newBits.copyOf(pages) + + for (page in 0 until pages) { + var diff = oldSafe[page] xor newSafe[page] + if (diff == 0L) continue + + val base = page shl 6 // 当前页的首个槽位 + while (diff != 0L) { + val bit = diff.countTrailingZeroBits() // 0..63 + val i = base + bit + if (i >= n) break + + val newGen = ((newSafe[page] ushr bit) and 1L) != 0L + if (newGen) createWithIndex(i) else removeWithIndex(i) + + diff = diff and (diff - 1) // 清除最低位的 1 + } + } + } + + + /** + * 服务端:生成下一个粒子(只改 位集 和计数,不生成粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * addSingle() + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断。 + * 方法从当前指针向后寻找第一个未显示槽位,必要时回绕;成功后会标记完整网络状态。 + * 服务端调用必须位于 Composition 生命周期更新所在的逻辑线程。 + */ + fun addSingle() { + if (count <= 0) return + val start = serverCurrentIndex.coerceIn(0, count - 1) + val idx = (start until count).firstOrNull { !isParticleDisplayed(it) } + ?: (0 until start).firstOrNull { !isParticleDisplayed(it) } + ?: return + + setBit(index.get(), idx, true) + displayedParticleCount++ + + // 客户端:立即生成该索引对应的粒子。 + if (client) { + createWithIndex(idx) + } + serverCurrentIndex = min(idx + 1, max(count - 1, 0)) + markDirty() + } + + /** + * 服务端:生成多个粒子(只改 位集 和计数,不生成粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * addMultiple(amount) + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * @param amount 生成数量(小于等于 0 时直接返回) + */ + fun addMultiple(amount: Int) { + if (amount <= 0) return + repeat(amount) { addSingle() } + } + + /** + * 服务端:移除上一个粒子(只改 位集 和计数,不删除粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * removeSingle() + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * 说明: + * - 从 `serverCurrentIndex` 向前寻找最近的已显示槽位,必要时回绕 + * - 成功后清除位、更新计数和指针,并标记完整网络状态 + * - 客户端场景会立即调用 `removeWithIndex`,服务端调用必须位于 Composition 生命周期更新线程 + */ + fun removeSingle() { + if (count <= 0) return + val start = serverCurrentIndex.coerceIn(0, count - 1) + val idx = (start downTo 0).firstOrNull(::isParticleDisplayed) + ?: (count - 1 downTo start + 1).firstOrNull(::isParticleDisplayed) + ?: return + + setBit(index.get(), idx, false) + displayedParticleCount-- + + // 客户端:立即删除该索引对应的粒子。 + if (client) { + removeWithIndex(idx) + } + serverCurrentIndex = max(idx - 1, 0) + markDirty() + } + + /** + * 服务端:移除多个粒子(只改 位集 和计数,不删除粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * removeMultiple(amount) + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * @param amount 移除数量(小于等于 0 时直接返回) + */ + fun removeMultiple(amount: Int) { + if (amount <= 0) return + repeat(amount) { removeSingle() } + } + + /** + * 重置所有粒子状态(清空 位集、计数器、指针) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * resetAll() + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * 说明: + * - 服务端:只清位集、计数和指针,等待同步到客户端后按差异处理 + * - 客户端:先遍历位集,把已生成的粒子对象全部通过 `removeWithIndex(i)` 移除,再清空位集 + * + * 注意: + * - resetAll() 会把 serverCurrentIndex 重置为 0 + * - displayedParticleCount 会重置为 0 + */ + fun resetAll() { + // 客户端:先删除所有已经生成的粒子对象。 + if (client && count > 0) { + val pages = pagesFor(count) + val bits = index.get() + for (page in 0 until min(bits.size, pages)) { + var v = bits[page] + if (v == 0L) continue + val base = page shl 6 + while (v != 0L) { + val bit = v.countTrailingZeroBits() + val i = base + bit + if (i >= count) break + removeWithIndex(i) + v = v and (v - 1) + } + } + } + + // 清空位集。 + val arr = index.get() + val changed = displayedParticleCount != 0 || serverCurrentIndex != 0 || arr.any { it != 0L } + for (i in arr.indices) arr[i] = 0L + + displayedParticleCount = 0 + serverCurrentIndex = 0 + if (changed) markDirty() + } + + override fun rotateToPoint(to: RelativeLocation) { + super.rotateToPoint(to) + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + super.rotateToWithAngle(to, radian) + } + + override fun rotateAsAxis(radian: Double) { + super.rotateAsAxis(radian) + } + + /** + * 直接设置一个序列槽位的显示状态。 + * + * 服务端真实变更后会标记完整网络状态,客户端真实变更后会立即创建或移除对应节点。 + * 越界或状态未变化时不会修改计数、节点和网络脏状态。 + * + * @param index 目标槽位索引,有效范围为 `0 until count` + * @param generated `true` 表示显示,`false` 表示移除 + */ + fun setParticleStatus(index: Int, generated: Boolean) { + if (index !in 0 until count || isParticleDisplayed(index) == generated) return + setBit(this.index.get(), index, generated) + displayedParticleCount += if (generated) 1 else -1 + if (client) { + if (generated) createWithIndex(index) else removeWithIndex(index) + } + markDirty() + } + + fun isParticleDisplayed(index: Int): Boolean { + if (index !in 0 until count) return false + return getBit(this.index.get(), index) + } + + final override fun toggleScaleDisplayed() { + if (!displayed) { + return + } + sequencedParticlesData.forEach { (data, location) -> + applyScale(data.uuid, location) + } + toggleRelative() + } + + /** + * 客户端使用的索引到 UUID 映射,用于按索引删除。 + * 服务器不会读取该映射。 + */ + private var indexToUuid: Array = emptyArray() + private fun createWithIndex(i: Int) { + if (!client) return + if (i !in 0 until sequencedParticlesData.size) return + if (indexToUuid.size != count) indexToUuid = arrayOfNulls(count) + + // 如果已经创建过就跳过(避免重复) + if (indexToUuid[i] != null) return + + val (data, rl) = sequencedParticlesData[i] + displayEntry(data, rl) + if (particles.containsKey(data.uuid)) { + indexToUuid[i] = data.uuid + } + } + + override fun displayEntry(data: CompositionData, pos: RelativeLocation) { + super.displayEntry(data, pos) + refreshGpuTransformMode() + } + + override fun trackDisplayedParticleLocation(pos: RelativeLocation) = Unit + + + private fun removeWithIndex(i: Int) { + if (!client) return + if (i !in 0 until count) return + if (indexToUuid.size != count) return + + val uuid = indexToUuid[i] ?: return + val obj = particles[uuid] ?: run { + indexToUuid[i] = null + return + } + + unregisterCParticleNode(obj) + obj.remove() + if (obj is Tickable<*>) { + controlerTicks.remove(obj) + } + particles.remove(uuid) + particleLocations.remove(obj) + indexToUuid[i] = null + refreshGpuTransformMode() + } + + + private fun ensureIndexCapacity(newCount: Int) { + val pages = pagesFor(newCount) + val current = index.get() + if (current.size == pages) return + + val resized = LongArray(pages) + // 把旧页复制到新数组 + val len = min(current.size, resized.size) + for (i in 0 until len) resized[i] = current[i] + index.setMemoValue(resized) + } + + private fun pagesFor(cnt: Int): Int { + if (cnt <= 0) return 0 + return (cnt + 63) / 64 + } + + private fun getBit(arr: LongArray, index: Int): Boolean { + if (arr.isEmpty()) return false + val page = index ushr 6 // /64 + if (page !in arr.indices) return false + val bit = index and 63 + return ((arr[page] ushr bit) and 1L) == 1L + } + + private fun setBit(arr: LongArray, index: Int, value: Boolean) { + if (arr.isEmpty()) return + val page = index ushr 6 + if (page !in arr.indices) return + val bit = index and 63 + val mask = 1L shl bit + arr[page] = if (value) (arr[page] or mask) else (arr[page] and mask.inv()) + } + +} 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..c6fc97c0 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt @@ -0,0 +1,203 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.StatusHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.TreeMap +import java.util.UUID + +/** + * - 只用于客户端嵌套(不允许单独发包生成) + * - 继承 SequencedParticleComposition:提供 SortedMap 序列数据(按 CompositionData.order 排序) + */ +class SequencedParticleShapeComposition(uuid: UUID) : SequencedParticleComposition(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 + + var spawnAge = 0 + + /** + * 是否在reversed 到0的时候清理粒子 (防止残留影响观感) + */ + var reversedClean = true + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + private set + + fun loadScaleHelper(min: Double, max: Double, scalingTick: Int): SequencedParticleShapeComposition { + scaleHelper = CompositionScaleHelper(min, max, scalingTick).apply { + loadControler(this@SequencedParticleShapeComposition) + } + scalePreTick = true + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): SequencedParticleShapeComposition { + scaleHelper = CompositionBezierScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + /** + * 改用apply是因为 要防止和addPreTickAction冲突 + */ + fun applyDisplayAction(action: SequencedParticleShapeComposition.() -> Unit): SequencedParticleShapeComposition { + invokes.add(action) + return this + } + + fun applyBeforeDisplayAction( + action: SequencedParticleShapeComposition.(SortedMap) -> Unit + ): SequencedParticleShapeComposition { + beforeInvokes.add(action) + return this + } + + fun applyPoint( + point: RelativeLocation, + dataSupplier: (RelativeLocation, Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to dataSupplier) + return this + } + + fun applyPointRel( + point: RelativeLocation, + dataSupplier: (RelativeLocation) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(rel) }) + return this + } + + fun applyPointI( + point: RelativeLocation, + dataSupplier: (Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(o) }) + return this + } + + + fun applyBuilder( + builder: PointsBuilder, + dataSupplier: (RelativeLocation, Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(builder to dataSupplier) + return this + } + + fun applyBuilderI( + builder: PointsBuilder, + dataSupplier: (Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(builder to { rel, o -> dataSupplier(o) }) + return this + } + + fun applyBuilderRel( + builder: PointsBuilder, + dataSupplier: (RelativeLocation) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(builder to { rel, o -> dataSupplier(rel) }) + return this + } + + fun setReversedScaleOnDisableStatus(status: StatusHelper): SequencedParticleShapeComposition { + addPreTickAction { + if (status.displayStatus == 2) { + scaleReversed = true + } + } + return this + } + + fun setReversedScaleOnCompositionStatus(composition: ParticleComposition): SequencedParticleShapeComposition { + addPreTickAction { + if (composition.status.getCurrentStatus() == StatusHelper.Status.DISABLE) { + scaleReversed = true + } + } + return this + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + throw NotImplementedError("此类只作为客户端嵌套使用, 不能单独生成! ") + } + + /** + * SequencedParticleComposition 需要返回 SortedMap + * TreeMap 会按 CompositionData.compareTo -> order 排序 + */ + override fun getParticleSequenced(): SortedMap { + val res: SortedMap = TreeMap() + var order = 0 + points.forEach { (builder, supplier) -> + res.putAll( + builder.createWithCompositionData { rel -> + supplier(rel, order++) + } + ) + } + return res + } + + + override fun beforeDisplaySequenced(map: SortedMap) { + super.beforeDisplaySequenced(map) + beforeInvokes.forEach { + it(map) + } + } + + override fun onDisplay() { + animate.clientOnly() + 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/manager/ParticleCompositionManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt new file mode 100644 index 00000000..f6b04a4f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt @@ -0,0 +1,379 @@ +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.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 { + /** + * 弱引用保存已进入客户端显示生命周期的 Composition 实例。 + * + * 示例:顶层 Composition 及其直接显示的子 Composition 会分别占一个条目。 + * 禁止在此处强持有 Composition,否则 Emitter 临时创建的实例无法被回收。 + */ + private val loadedClientCompositions = WeakIdentitySet() + + val clientView = ConcurrentHashMap() + + val serverView = ConcurrentHashMap() + + /** + * 只有玩家可见才会处理发包 + */ + val playerPlayerVisibleSet = ConcurrentHashMap>() + + val registeredTypes = ConcurrentHashMap>() + + /** + * 登记或移除一个客户端活动 Composition。 + * + * 示例:Composition 首次 `display()` 时传入 `true`,执行 `clear(true)` 时传入 `false`。 + * 禁止为服务端仅用于同步的 Composition 传入 `true`。 + * + * @param composition 需要更新活动状态的 Composition 实例 + * @param loaded `true` 表示已加载到客户端,`false` 表示已离开客户端生命周期 + */ + internal fun setClientLoaded(composition: ParticleComposition, loaded: Boolean) { + if (loaded) { + loadedClientCompositions.add(composition) + } else { + loadedClientCompositions.remove(composition) + } + } + + /** + * 返回客户端当前活动的全部 Composition 实例数量。 + * + * 示例:F3 调试信息用该值统计顶层、嵌套及 Emitter 直接显示的 Composition。 + * 禁止把该值理解为服务端实例数或已注册类型数。 + * + * @return 当前客户端活动 Composition 数量 + */ + @JvmStatic + fun loadedClientCount(): Int = loadedClientCompositions.size() + + /** 返回当前客户端活动的全部 Composition 快照,包含嵌套实例。 */ + internal fun debugCompositions(): List = loadedClientCompositions.snapshot() + + /** 返回服务端当前活动 Composition 实例数。 */ + 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(), registryAccess) + 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) + } + } + } + + /** + * 清理当前客户端世界持有的所有 Composition。 + * + * 示例:客户端断线或换世界时调用该方法,将顶层与嵌套实例一并移出活动计数。 + * 禁止用该方法清理服务端 [serverView]。 + */ + fun clearClient() { + // 这里必须走 clear(true) 强制销毁 + // remove() 可能被使用者重写成延迟消散的语义 (例如先 status.disable() 等渐隐结束再真正销毁) + // 客户端断连/换世界要求立刻干净 否则 composition 会脱离 clientView 变成孤儿粒子 + clientView.values.forEach { + it.clear(true) + } + clientView.clear() + loadedClientCompositions.clear() + } + + + fun clearServer() { + // 同 clearClient 不能依赖使用者重写的 remove() + 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..20e2b631 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt @@ -0,0 +1,26 @@ +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) +} \ No newline at end of file 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..acb79b08 --- /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) +} \ No newline at end of file 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..2fb0b504 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt @@ -0,0 +1,24 @@ +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) +} \ No newline at end of file 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..e342b4b6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.network.particle.data + +abstract class RangeData>(var min: T, var max: T) { + init { + require(min <= max) { "min must be <= $max" } + } +} \ No newline at end of file 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..1a8b4a4c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt @@ -0,0 +1,29 @@ +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.network.PacketByteBuf + +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * 自动生成网络 codec 并以实现类完整类名注册的 [ClassEmitters]。 + * + * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; + * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, + * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 + * + * @param pos 发射器生成位置 + * @param world 发射器生效世界 + */ +abstract class AutoEmitters(pos: Vec3, world: Level?) : ClassEmitters(pos, world) { + override fun getEmittersID(): String { + return this::class.java.name + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + 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..a65b1c19 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt @@ -0,0 +1,32 @@ +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.network.PacketByteBuf + +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * 自动生成网络 codec 并以实现类完整类名注册的 [ClassParticleEmitters]。 + * + * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; + * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, + * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 + * + * @see ParticleEmittersRegistryHelper + * @constructor 实现类必须提供空构造方法或 `(Vec3, Level?)` 构造方法 + * + * @param pos 发射器生成位置 + * @param world 发射器生效世界 + */ +abstract class AutoParticleEmitters(pos: Vec3, world: Level?) : ClassParticleEmitters(pos, world) { + override fun getEmittersID(): String { + return this::class.java.name + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + 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..ea7b55e4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt @@ -0,0 +1,29 @@ +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.network.PacketByteBuf + +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * 自动生成网络 codec 并以实现类完整类名注册的 [TransformableCParticleEmitter]。 + * + * 实现类必须标注 [CooAutoRegister],并提供公开的 `(Vec3, Level?)` 构造器。 + * 普通同步字段可以使用 [CodecField];会在运行时修改的 Kotlin 属性建议使用 + * `var value by dirty(initialValue)`,赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 + * 基类生命周期和变换字段会自动编码。 + * + * @param pos 发射器世界坐标 + * @param world 发射器所在世界 + */ +abstract class AutoTransformableCParticleEmitter(pos: Vec3, world: Level?) : + TransformableCParticleEmitter(pos, world) { + final override fun getEmittersID(): String = this::class.java.name + + final override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt new file mode 100644 index 00000000..01992fcc --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +/** + * 定义 [TransformableCParticleEmitter] 新生成粒子使用的坐标空间。 + * + * [LOCAL] 把粒子位置和速度保存在发射器局部空间中。粒子生成后仍会跟随发射器平移、 + * [TransformableCParticleEmitter.emitterRotation] 和缩放, + * 也是默认模式。[WORLD] 使用普通 GPU 发射器的世界坐标语义,已经生成的粒子不再受发射器变换影响。 + * 模式切换只影响后续生成的粒子,已有粒子继续留在各自创建时的空间中。 + * + * @property networkId 稳定的网络编号,不依赖枚举声明顺序 + */ +enum class CParticleEmitterSpace(val networkId: Int) { + /** 粒子属于发射器局部空间,并持续跟随发射器变换。 */ + LOCAL(0), + + /** 粒子生成后保留在世界空间,不跟随发射器变换。 */ + WORLD(1); + + companion object { + /** + * 按稳定网络编号恢复发射空间。 + * + * @param networkId 网络数据中的空间编号 + * @return 对应的发射空间 + * @throws IllegalArgumentException 编号未知时抛出 + */ + @JvmStatic + fun fromNetworkId(networkId: Int): CParticleEmitterSpace { + return entries.firstOrNull { it.networkId == networkId } + ?: throw IllegalArgumentException("unknown CParticle emitter space: $networkId") + } + } +} 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..fde910eb --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt @@ -0,0 +1,411 @@ +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 + +/** + * 通用发射器实现,兼容多种可控对象: + * - [ControlableParticleData] + * - [ParticleComposition] + * - [DisplayEntity] + */ +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>() + + /** + * 是否启用线段插值器。 + * + * 如果设置为 `true`,每次发射会沿插值结果逐点生成;设置为 `false` 时,每 tick 只执行一次生成。 + */ + var enableInterpolator = false + + /** + * 发射器位移插值器。 + * + * 仅在 [enableInterpolator] 为 `true` 时生效,可通过 `setRefiner` 调整细分密度。 + */ + 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) + } + + /** 写法:先在 codec 的 decode 中创建对象,再把 `buf` 和 `container` 传入此方法,最后继续解码子类字段。 */ + 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) + } + + /** 质量,单位 g。 */ + 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 + } + + /** + * 服务器和客户端都会执行此方法。 + * + * 如需仅在服务端执行,请自行判断: + * ```kotlin + * if (!world!!.isClientSide) { ... } + * ``` + */ + abstract fun doTick() + + /** + * 可控对象生成器。 + * + * @param lerpProgress 发射器位移插值器的插值进度 + */ + abstract fun genControls(lerpProgress: Float): List> + + /** + * 在一次生成前会执行。 + * + * @param current 当前插值点的位置 + * @param lerpProgress 当前生成进度 + */ + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + /** + * 通用可控对象的生成后回调。 + * + * @param data 当前生成对象的数据 + * @param spawnPos 生成位置(对象创建后再改该值无效) + * @param particleLerpProgress 当前对象在本次批量生成中的进度 + * @param posLerpProgress 发射器位移插值进度(未启用插值时恒为 1) + */ + abstract fun singleControlableAction( + controler: Controlable<*>, + data: SerializableData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + /** + * 处理单个 controler 的移动方案 + */ + protected open fun moveSingleControler( + controler: Controlable<*>, + data: SerializableData, + to: Vec3, + collide: BlockHitResult + ) { + controler.teleportTo(to) + } + + /** + * 读取当前 controler 世界坐标。 + * + * 子类可重写以支持自定义可控对象,默认支持: + * - [ControlableParticle] + * - [ParticleComposition] + * - [DisplayEntity] + */ + 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 + } + } + + /** + * 读取当前 controler 的速度向量。 + * + * 子类可重写以支持自定义数据类型;默认对 [ControlableParticleData] 生效。 + */ + 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) + } + } + + + /** + * 数据同步需要实现此方法。 + * + * @param emitters 需要同步到当前实例的发射器模板 + */ + 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..05c67996 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt @@ -0,0 +1,594 @@ +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>() + + /** + * 是否启用线段插值器 如果设置为true 则spawnParticles由线段插值器管控 设置为false,则只会在 + * 1tick内执行一次spawnParticles + */ + var enableInterpolator = false + + /** + * 插值器工具 需要启用 enableInterpolator 可以修改 + * + * @see enableInterpolator + */ + var emittersInterpolator: Interpolator = LineEmitterInterpolator() + .setRefiner(5.0) + + override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + val handlerID = handler.getHandlerID() + // 自动注册不适用于多人 + // TODO + 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 { + // 保留innerClass为false的 + 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) + } + + /** 写法 先在codec的 decode方法中 创建此对象 然后将buf和container 传入此方法 然后继续decode自己的参数 */ + 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) + } + + /** + * 旧版 GPU 力场列表接口。 + * + * 旧 emitter 可以继续覆写此方法;桥接层会在每 tick 调用 + * [submitCParticleForces],由默认实现把列表内容提交到 sink。 + * 新 emitter 应直接覆写流式接口,避免创建临时列表。 + * + * @return 按执行顺序排列的力场列表 + */ + open fun cparticleForces(): List = emptyList() + + /** + * GPU 模式附加力场 (每 tick 同步一次)。 + * + * 默认实现兼容旧版 [cparticleForces];新 emitter 可以直接向 sink 提交, + * 从而避免每 tick 构建临时列表。内置 ParticleCommand 可用 + * [CParticleForce.fromCommand] 转换。 + * + * @param sink 当前 emitter 的力场快照,调用期间只应提交本 emitter 的力 + */ + open fun submitCParticleForces(sink: CParticleForceSink) { + sink.submitAll(cparticleForces()) + } + + /** + * CParticle 方块碰撞相对 system 原点的最大保证范围,单位为方块。 + * + * 示例:粒子最远会离开 system 原点 40 格时返回 `40`。 + * 禁止:返回值会被限制在 `0..96`;范围越大,网格刷新成本按立方增长。 + */ + open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE + + /** 质量 单位 g */ + 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, + ) + } + } + + /** + * 服务器和客户端都会执行此方法 判断服务器清使用 + * + * ```kotlin + * if(!world!!.isClient) + * ``` + */ + abstract fun doTick() + + /** + * 粒子样式生成器 + * @param lerpProgress 粒子发射器位移插值器的插值进度 + * */ + abstract fun genParticles(lerpProgress: Float): List> + + /** + * 在一次粒子生成前会执行 + * + * @param current 当前插值的生成位置 + * @param lerpProgress 粒子生成的进度 + */ + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + /** + * 如若要修改粒子的位置, 速度 属性 请直接修改 ControlableParticleData + * + * @param data 用于操作单个粒子属性的类 + * @param spawnPos 生成的位置,在粒子已经生成后再修改无效 + * 执行tick方法请使用controler.addPreTickAction + * @param particleLerpProgress + * 生成这个粒子的时候,当前的进度((当前生成的粒子索引+1)/genParticles(lerpProgress: Float).size ) + * @param posLerpProgress 当发射器进行发射插值时, 插值的偏移 如果不使用插值则永远为1 + */ + abstract fun singleParticleAction( + controler: ParticleControler, + data: ControlableParticleData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + /** + * 当粒子死亡(age >= maxAge然后remove)时 会执行这个方法 + * + * @param oldControler + * @param oldData + * @param respawnCount 重新生成的次数 (0代表第一次从emitter生成) + * @param reason 粒子移除原因 + * @return 会根据data生成新粒子 第二个pair是和粒子死亡位置的相对位置(不是发射器相对位置) + */ + 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 + } + // cparticle GPU 路径: 数据直接进 GPU 粒子系统, 跳过 controler/事件/碰撞 + 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 { + // 针对 ParticleHitEntityEvent + 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 { + // 针对 ParticleOnLiquidEvent + 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 + // 判断现在的位置是不是液体 + if (!world.shouldTickBlocksAt(blockPos)) { + return@addPreTickAction + } + val state = world.getBlockState(blockPos) + val currentLiquid = !state.isSolid + control.bufferedData["cross_liquid"] = currentLiquid + if (beforeLiquid || !currentLiquid) { + return@addPreTickAction + } + // 前一个tick不是液体 当前tick是液体则触发事件 + 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) { + // 找方向 velocity + handlerList[ParticleOnGroundEvent.EVENT_ID]?.let { + val offset = clipRes.direction.normal.asVec3() * 0.1 + val event = ParticleOnGroundEvent( + this, + data, + ofFloored(prepareMove), + clipRes.location.add(offset), + clipRes + ) + for ((handler, _) in it) { + if (handler.getTargetEventID() != ParticleOnGroundEvent.EVENT_ID) { + continue + } + handler.handle(event) + if (event.canceled) { + break + } + } + } + } + + if (clipRes.type != HitResult.Type.MISS) { + handlerList[ParticleCollideEvent.EVENT_ID]?.let { + val event = ParticleCollideEvent( + this, data, clipRes + ) + for ((handler, _) in it) { + 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) + } + + + /** + * # 处理单个粒子的位移位置 + * - 方便更真实的物理模拟 + * - 用于修改particle受到velocity时的移动 + * + * @param particle 被操作位置的粒子 + * @param data 被操作位置的粒子数据 + * @param to 正常情况下粒子应该移动到的位置(单纯loc+velocity) + * @param collide 粒子的碰撞情况 (如果显示碰撞,则代表to位置存在方块 loc不存在) + */ + protected open fun moveSingleParticleWithVelocity( + particle: ControlableParticle, + data: ControlableParticleData, + to: Vec3, + collide: BlockHitResult + ) { + particle.teleportTo(to) + } + + /** + * 数据同步需要实现此方法 + * + * @param emitters 更新的模板发射器 + */ + 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..3456adf7 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt @@ -0,0 +1,117 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.SerializableData +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import io.netty.buffer.Unpooled +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.network.PacketByteBuf +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * SerializableData wrapper for ParticleComposition emitters entry. + */ +class CompositionEmittersData( + var compositionType: String = "", + var compositionData: ByteArray = ByteArray(0) +) : SerializableData { + constructor(composition: ParticleComposition) : this() { + setComposition(composition) + } + + companion object { + @JvmStatic + val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, data -> + buf.writeUtf(data.compositionType) + buf.writeInt(data.compositionData.size) + buf.writeBytes(data.compositionData) + }, + { buf -> + val type = buf.readUtf() + val size = buf.readInt() + val bytes = ByteArray(size) + buf.readBytes(bytes) + CompositionEmittersData(type, bytes) + } + ) + + @JvmStatic + fun fromComposition(composition: ParticleComposition): CompositionEmittersData { + val buf = PacketByteBuf(Unpooled.buffer()) + composition.getCodec().encode(buf, composition) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + return CompositionEmittersData(composition::class.java.name, data) + } + } + + private var prepared: ParticleComposition? = null + + private fun decodeComposition(): ParticleComposition { + val codec = ParticleCompositionManager.registeredTypes[compositionType] + ?: throw IllegalStateException("ParticleComposition codec not registered for type: $compositionType") + return codec.decode(PacketByteBuf(Unpooled.wrappedBuffer(compositionData.copyOf()))) + } + + private fun cloneComposition(composition: ParticleComposition): ParticleComposition { + val type = composition::class.java + val ins = runCatching { + type.getDeclaredConstructor(Vec3::class.java, Level::class.java) + .apply { isAccessible = true } + .newInstance(composition.position, composition.world) + }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() + + return ins.apply { update(composition) } + } + + private fun resolveComposition(): ParticleComposition { + return prepared ?: decodeComposition().also { prepared = it } + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return PACKET_CODEC + } + + override fun clone(): SerializableData { + val composition = runCatching { cloneComposition(resolveComposition()) }.getOrNull() + return if (composition != null) { + CompositionEmittersData(composition) + } else { + CompositionEmittersData(compositionType, compositionData.copyOf()) + } + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + val composition = resolveComposition() + composition.world = world + composition.setPositionWithoutToggle(pos) + prepared = composition + return composition + } + + override fun getDisplayer(): ParticleDisplayer { + val composition = resolveComposition() + return ParticleDisplayer.withComposition(composition) + } + + fun setComposition(composition: ParticleComposition): CompositionEmittersData { + val clone = cloneComposition(composition) + val data = fromComposition(clone) + compositionType = data.compositionType + compositionData = data.compositionData + prepared = clone + return this + } +} 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..469e16b3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt @@ -0,0 +1,266 @@ +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 net.minecraft.network.PacketByteBuf + +import org.joml.Vector3f + +/** + * 单个 CParticle GPU 粒子的完整生成数据。 + * + * 额外纹理蒙版、生命周期曲线、旋转和 GPU 更新方式都属于每份 data,且本类型只让当前 data + * 进入 CParticle 路径。同一次 + * `genParticles()` 可以混合本类型与普通 [ControlableParticleData]:前者走 GPU,后者继续执行 + * `singleParticleAction` 和 [sign] 对应的 CPU 行为。基础纹理始终来自父类 [effect], + * [textureSource] 只控制叠加在基础纹理上的可选蒙版。 + * + * Example: + * ```kotlin + * val block = ControlableCParticleData().apply { + * textureSource = textureOfBlock(state) + * } + * val item = ControlableCParticleData().apply { + * textureSource = textureOfItem(stack) + * } + * ``` + * Forbidden: 不要把一次 emitter 调用中的公共纹理写回 emitter 单例。 + */ +open class ControlableCParticleData : ControlableParticleData() { + /** + * # STATIC + * 生成后不保留本 data; + * + * # DYNAMIC + * 允许同步可变外观 + * + * 不建议使用DYNAMIC,因为GPU粒子不支持singleAction, + * 这会导致你必须得额外存储每一个粒子的Data,并且按照对应的方式自己修改 + * 非常浪费性能的同时,也不是这个API希望的做法, + * 颜色、生命周期透明度和缩放都已有 STATIC 曲线,不需要为这类变化使用 DYNAMIC。 + * + * 按照GPU粒子的设定一般使用STATIC + * */ + var updateMode: CParticleUpdateMode = CParticleUpdateMode.STATIC + + /** + * 叠加在 [effect] 基础纹理上的可选蒙版;`null` 表示不叠加蒙版。 + * + * Example: 同一 emitter 的不同 CParticle data 可以叠加不同方块或物品纹理。 + * Forbidden: 生成后的 STATIC 粒子不会继续引用或观察此对象。 + */ + var textureSource: CParticleTextureSource? = null + + /** 按 `age / maxAge` 在 GPU 中采样的不透明度乘数曲线。 */ + var alphaCurve: CParticleCurve? = null + + /** + * 按 `age / maxAge` 同时缩放 X/Y 尺寸的等比 GPU 曲线。 + * + * 最终会与 [scaleXCurve]、[scaleYCurve] 的对应轴倍率相乘。 + * Example: `scaleCurve = CParticleCurve.fadeInOut()` 会保持当前宽高比例。 + * Forbidden: 不要用它单独控制某一个轴。 + */ + var scaleCurve: CParticleCurve? = null + + /** + * 按 `age / maxAge` 只缩放 X 方向尺寸的 GPU 曲线。 + * + * 示例:从 `0.2F` 变化到 `1F` 可以让粒子横向展开。 + * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 + */ + var scaleXCurve: CParticleCurve? = null + + /** + * 按 `age / maxAge` 只缩放 Y 方向尺寸的 GPU 曲线。 + * + * 示例:从 `1F` 变化到 `0F` 可以让粒子纵向收拢。 + * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 + */ + var scaleYCurve: CParticleCurve? = null + + /** 按 `age / maxAge` 与基础颜色相乘的 GPU RGB 曲线。 */ + var colorCurve: CParticleColorCurve? = null + + /** ROTATION 模式的指向向量;`null` 时使用父类 yaw/pitch。 */ + var rotationDirection: Vector3f? = null + + /** 欧拉角速度,单位 rad/tick;x=pitch、y=yaw、z=roll。 */ + var angularVelocity: Vector3f = Vector3f() + + /** 每个整数 tick 使用稳定随机种子重新选择纹理动画帧。 */ + var randomAgePreTick: Boolean = false + + /** GPU 随机帧和随机 UV 的种子;`null` 表示生成时自动分配。 */ + var randomSeed: Int? = null + + /** + * 是否使用 CParticle 共享方块占用网格处理本粒子的位移碰撞。 + * + * 碰撞响应与 `moveSingleParticleWithVelocity` 的常用实现一致:命中后停在表面前, + * 并移除速度的法线分量。复杂 `VoxelShape` 会按完整方块近似。 + * 共享缓存覆盖 system 原点附近的 `64³` 方块,分桶边缘至少保留 24 格;缓存外按未命中处理。 + * Example: 同一次 `genParticles()` 可以只给落尘 data 设置为 `true`。 + * Forbidden: 不要用它代替需要台阶、栅栏或实体精确形状的 CPU 碰撞。 + */ + var blockCollision: Boolean = false + + /** 供 Force Command 的 CommandMask 选择。 */ + var commandMask: Int = 0 + + /** metadata 扩展标志位。 */ + var metadataFlags: Int = 0 + + /** 粒子电荷;NaN 表示由 Charge command 使用默认值。 */ + var charge: Float = Float.NaN + + /** 粒子半径,供单点 Lennard-Jones 使用。 */ + var radius: Float = 0F + + /** + * 保存专用 data 的网络编解码器。 + * + * Example: `@CodecField var template = ControlableCParticleData()` 会通过此 codec 同步。 + * Forbidden: 不要用父类 codec 解码本类型,否则 [textureSource] 会丢失。 + */ + companion object { + /** + * 先处理继承的基础字段,再处理可空纹理来源。 + * + * Example: 编解码后 `sign`、velocity、effect 和 textureSource 都保持不变。 + * Forbidden: 字段顺序不能与 [decode] 分离修改。 + */ + @JvmField + val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of(::encode, ::decode) + + /** + * 编码基础字段和当前 data 的可选纹理来源。 + * + * Example: `null` 只写入一个 false,客户端继续使用 effect。 + * Forbidden: 不要在这里解析模型或 UV。 + * + * @param buf 目标网络缓冲区 + * @param data 要编码的 CParticle data + */ + private fun encode(buf: PacketByteBuf, data: ControlableCParticleData) { + encodeBase(buf, data) + val source = data.textureSource + buf.writeBoolean(source != null) + if (source != null) { + CParticleTextureSource.STREAM_CODEC.encode(buf, source) + } + buf.writeByte(data.updateMode.ordinal) + writeNullable(buf, data.alphaCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.scaleCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.scaleXCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.scaleYCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.colorCurve, CParticleColorCurve.STREAM_CODEC) + val direction = data.rotationDirection + buf.writeBoolean(direction != null) + if (direction != null) buf.writeVector3f(direction) + buf.writeVector3f(data.angularVelocity) + buf.writeBoolean(data.randomAgePreTick) + val seed = data.randomSeed + buf.writeBoolean(seed != null) + if (seed != null) buf.writeInt(seed) + buf.writeBoolean(data.blockCollision) + buf.writeInt(data.commandMask) + buf.writeInt(data.metadataFlags) + buf.writeFloat(data.charge) + buf.writeFloat(data.radius) + } + + /** + * 解码基础字段,并在同一个子类实例上恢复纹理来源。 + * + * Example: Item 来源解码后仍持有独立的 ItemStack 快照。 + * Forbidden: 不能先通过父类 codec 解码后再强制转换。 + * + * @param buf 来源网络缓冲区 + * @return 完整的 CParticle data + */ + private fun decode(buf: PacketByteBuf): ControlableCParticleData { + val data = decodeBase(buf, ControlableCParticleData()) + if (buf.readBoolean()) { + data.textureSource = CParticleTextureSource.STREAM_CODEC.decode(buf) + } + val updateModeOrdinal = buf.readUnsignedByte().toInt() + require(updateModeOrdinal < CParticleUpdateMode.entries.size) { + "unknown CParticle update mode: $updateModeOrdinal" + } + data.updateMode = CParticleUpdateMode.entries[updateModeOrdinal] + data.alphaCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.scaleCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.scaleXCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.scaleYCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.colorCurve = readNullable(buf, CParticleColorCurve.STREAM_CODEC) + if (buf.readBoolean()) data.rotationDirection = buf.readVector3f() + data.angularVelocity = buf.readVector3f() + data.randomAgePreTick = buf.readBoolean() + if (buf.readBoolean()) data.randomSeed = buf.readInt() + data.blockCollision = buf.readBoolean() + data.commandMask = buf.readInt() + data.metadataFlags = buf.readInt() + data.charge = buf.readFloat() + data.radius = buf.readFloat() + return data + } + + private fun writeNullable( + buf: PacketByteBuf, + value: T?, + codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec, + ) { + buf.writeBoolean(value != null) + if (value != null) codec.encode(buf, value) + } + + private fun readNullable( + buf: PacketByteBuf, + codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec, + ): T? = if (buf.readBoolean()) codec.decode(buf) else null + } + + /** + * 返回包含纹理来源的专用 codec。 + * + * Example: emitter 自动编码器可从实例查询此 codec。 + * Forbidden: 返回父类 codec 会截断扩展字段。 + * + * @return [PACKET_CODEC] + */ + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = PACKET_CODEC + + /** + * 复制基础字段和当前纹理来源,返回独立的 [ControlableCParticleData]。 + * + * Example: emitter 模板可以为每个生成结果调用 `clone()`。 + * Forbidden: 派生类若有额外字段,必须覆盖本方法,不能依赖本实现保留派生类型。 + * + * @return 带有新 UUID 的 CParticle data + */ + override fun clone(): ControlableCParticleData { + return ControlableCParticleData().also { + copyTo(it) + it.updateMode = updateMode + it.textureSource = textureSource + it.alphaCurve = alphaCurve + it.scaleCurve = scaleCurve + it.scaleXCurve = scaleXCurve + it.scaleYCurve = scaleYCurve + it.colorCurve = colorCurve + it.rotationDirection = rotationDirection?.let(::Vector3f) + it.angularVelocity = Vector3f(angularVelocity) + it.randomAgePreTick = randomAgePreTick + it.randomSeed = randomSeed + it.blockCollision = blockCollision + it.commandMask = commandMask + it.metadataFlags = metadataFlags + it.charge = charge + it.radius = radius + } + } +} 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..1fcc517a --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt @@ -0,0 +1,458 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.CooParticlesConstants +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.ParticleTypes +import net.minecraft.network.PacketByteBuf +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: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + ::encodeBase, + { buf -> decodeBase(buf, ControlableParticleData()) }, + ) + + /** + * 编码所有基础粒子字段,供父类和扩展 data 共用。 + * + * Example: `ControlableCParticleData` 先调用此方法,再写入自己的纹理来源。 + * Forbidden: 子类不能改变这些字段的顺序,否则旧的基础 data 无法解码。 + * + * @param buf 目标网络缓冲区 + * @param data 要编码的基础粒子数据 + */ + 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) + + ParticleTypes.STREAM_CODEC.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) + } + + /** + * 从缓冲区读取基础字段并写入指定实例。 + * + * Example: 子类传入自己的新实例,解码后仍保留实际运行时类型。 + * Forbidden: 不要在这里固定构造 `ControlableParticleData`,否则子类字段会丢失。 + * + * @param buf 来源网络缓冲区 + * @param target 接收基础字段的实例 + * @return 赋值完成的 [target] + */ + internal fun decodeBase( + buf: PacketByteBuf, + target: T, + ): T { + val uuid = buf.readUUID() + val velocity = buf.readVec3() + val weightSize = buf.readFloat() + val heightSize = buf.readFloat() + val uniformSize = buf.readBoolean() + val visibleRange = buf.readFloat() + val color = buf.readVector3f() + val alpha = buf.readFloat() + val age = buf.readInt() + val maxAge = buf.readInt() + val textureSheet = buf.readUtf() + val effect = ParticleTypes.STREAM_CODEC.decode(buf) as ControlableParticleEffect + effect.controlUUID = uuid + val speed = buf.readDouble() + val speedLimit = buf.readDouble() + val sign = buf.readInt() + val light = buf.readInt() + val cameraOption = ParticleCameraOption.STREAM_CODEC.decode(buf) + val axis = buf.readVec3() + val yaw = buf.readFloat() + val pitch = buf.readFloat() + val roll = buf.readFloat() + val depthSize = buf.readFloat() + return target.apply { + this.uuid = uuid + this.velocity = velocity + this.color = color + this.alpha = alpha + this.uniformSize = uniformSize + this.weightSize = weightSize + this.heightSize = heightSize + this.depthSize = depthSize + this.visibleRange = visibleRange + this.age = age + this.maxAge = maxAge + this.textureSheet = textureSheet + this.effect = effect + this.speed = speed + this.sign = sign + this.speedLimit = speedLimit + this.light = light + this.cameraOption = cameraOption + this.axis = axis + this.yaw = yaw + this.pitch = pitch + this.roll = roll + } + } + + @JvmStatic + fun registerRenderType(type: ParticleRenderType) { + particleTexturesMapper[type.toString()] = type + } + } + + + /** + * 粒子生成时会传输的控制UUID + */ + var uuid = UUID.randomUUID() + + /** + * 粒子的移动向量 + * 在粒子发射器中 会不断调用这次的参数 + * + * 此选项会一直赋值给实际粒子 + */ + var velocity: Vec3 = Vec3.ZERO + + /** + * 生成的粒子相机朝向模式,默认始终面向摄像头。 + */ + var cameraOption: ParticleCameraOption = ParticleCameraOption.BILLBOARD + + /** 旧布尔 API 的兼容桥接:true = BILLBOARD,false = ROTATION。 */ + var faceToCamera: Boolean + get() = cameraOption == ParticleCameraOption.BILLBOARD + set(value) { + cameraOption = ParticleCameraOption.fromFaceToCamera(value) + } + + /** AXIS_BILLBOARD 使用的固定轴方向。 */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 如果faceToCamera为false + * 则此参数代表了粒子水平朝向 + * + * 弧度制 + */ + var yaw = 0.0f + + /** + * 如果faceToCamera为false + * 则此参数代表了粒子垂直朝向 + * + * 弧度制 + * + */ + var pitch = 0.0f + + /** + * 此参数代表了粒子滚动朝向 + * + * 弧度制 + * + */ + var roll = 0.0f + + /** 是否保持宽高等比。 */ + var uniformSize = true + + /** 粒子宽度大小。 */ + private var currentWeightSize = 0.2f + var weightSize: Float + get() = currentWeightSize + set(value) { + currentWeightSize = value + if (uniformSize) { + currentHeightSize = value + } + } + + /** 粒子高度大小。 */ + private var currentHeightSize = 0.2f + var heightSize: Float + get() = currentHeightSize + set(value) { + currentHeightSize = value + if (uniformSize) { + currentWeightSize = value + } + } + + private var currentDepthSize = 0f + var depthSize: Float + get() = currentDepthSize + set(value) { + currentDepthSize = value + } + + /** 旧 size 兼容属性:设置时同时修改宽高。 */ + var size: Float + get() = (weightSize + heightSize) / 2f + set(value) { + currentWeightSize = value + currentHeightSize = value + } + + /** + * 粒子生成时采用的不透明度 + * + */ + var alpha = 1f + + /** + * 粒子生成时设置的age + * + */ + var age = 0 + + /** + * 粒子最大生命周期 + * + */ + var maxAge = 120 + + /** + * 粒子生成时的亮度 + * (修改粒子亮度时请修改该数据) + * + */ + var light = 15 + + /** + * 粒子可见范围 + */ + var visibleRange = 256f + + /** + * 粒子样式 (必须是可控制的粒子) + * + * 此选项只生效一次 + */ + var effect: ControlableParticleEffect = ControlableEndRodEffect(uuid) + + /** + * 一些特殊标识 + * 用于在single 区分不同类型 分工的粒子 + */ + var sign = 0 + + /** + * 粒子移动速度上限 + * 防止不知道什么原因导致粒子移速过高从而导致客户端卡死 + */ + var speedLimit = 32.0 + + /** + * 粒子生成时采用的颜色,后续控制不生效 + * + * 此选项只应用一次 + */ + var color = Vector3f(1f, 1f, 1f) + + /** + * 粒子重生次数 + * 他会在不断的粒子重生中递增 + */ + var respawnCount = 0 + internal set + + // 脑瘫东西设置了客户端专属 + // 粒子渲染方式 只生效一次 + private var textureSheet: String = "PARTICLE_SHEET_TRANSLUCENT" + + /** + * 粒子移动速度 + * 在ClassParticlesEmitters默认不生效 + */ + var speed: Double = 1.0 + fun textureSheetFromString(sheet: String): ParticleRenderType? { + return particleTexturesMapper[sheet] + } + + /** + * 快速旋转 yaw pitch 到目标点 + * + * @param to 目标相对位置 + */ + fun setRotationTo(to: Vector3f) { + val (x, y, z) = Math3DUtil.calculateEulerAnglesToPoint(to) + this.yaw = y + this.pitch = x + } + + /** + * 快速旋转 yaw pitch 到目标点 + * + * @param to 目标相对位置 + */ + fun setRotationTo(to: Vec3) { + setRotationTo(to.toVector3f()) + } + + /** + * 快速旋转 yaw pitch 到目标点 + * + * @param to 目标相对位置 + */ + fun setRotationTo(to: RelativeLocation) { + setRotationTo(to.toVector3f()) + } + + fun setAxisLocation(axis: RelativeLocation) { + this.axis = Vec3(axis.x, axis.y, axis.z) + } + + fun getTextureSheet(): ParticleRenderType { + return textureSheetFromString(textureSheet) ?: let { + CooParticlesConstants.logger.error("can not find textureSheet $textureSheet you need use ControlableParticleData.registerRenderType() to register mapper") + ParticleRenderType.PARTICLE_SHEET_OPAQUE + } + } + + /** + * 如果你的参数暴露在外面 可能会被服务器调用 + * 则使用这个 + * + * 输入的参数是你的目标的RenderType的toString的内容 + * + * @param value + */ + fun setTextureSheet(value: String) { + this.textureSheet = value + } + + fun setTextureSheet(value: TextureSheetsEnum){ + this.textureSheet = value.name + } + + /** + * 如果你的参数暴露在外面 (可能会被服务器环境调用) + * 那就不要使用这个方法 使用字符串的 + * 因为脑残的设计导致服务器无法访问 ParticleRenderType 类 + * [setTextureSheet(String)] + * + * @param value + */ + fun setTextureSheet(value: ParticleRenderType) { + this.textureSheet = value.toString() + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return PACKET_CODEC + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + val control = ControlParticleManager.createControl(effect.controlUUID) + val data = this + control.applyInitializedAction { + this.uniformSize = data.uniformSize + this.weightSize = data.weightSize + this.heightSize = data.heightSize + this.depthSize = data.depthSize + this.color = data.color + this.currentAge = data.age + this.lifetime = data.maxAge + this.light = data.light + this.textureSheet = data.getTextureSheet() + this.particleAlpha = data.alpha + this.cameraOption = data.cameraOption + this.axis = data.axis + this.previewAxis = data.axis + this.previewWeightSize = data.weightSize + this.previewHeightSize = data.heightSize + this.previewDepthSize = data.depthSize + this.currentPitch = data.pitch + this.currentYaw = data.yaw + this.currentRoll = data.roll + this.previewPitch = data.pitch + this.previewYaw = data.yaw + this.previewRoll = data.roll + } + return control + } + + override fun getDisplayer(): ParticleDisplayer { + return ParticleDisplayer.withSingle(effect) + } + + override fun clone(): ControlableParticleData { + return ControlableParticleData().also(::copyTo) + } + + /** + * 把可克隆的基础字段复制到另一个 data,并为目标生成新 UUID。 + * + * Example: 扩展类可调用 `super.copyTo(target)` 后补充自己的字段。 + * Forbidden: 网络解码不能调用此方法,因为网络中的 UUID 必须原样保留。 + * + * @param target 接收当前基础字段的实例 + */ + protected fun copyTo(target: ControlableParticleData) { + target.uuid = UUID.randomUUID() + target.velocity = velocity + target.uniformSize = uniformSize + target.weightSize = weightSize + target.heightSize = heightSize + target.depthSize = depthSize + target.color = color + target.alpha = alpha + target.visibleRange = visibleRange + target.age = age + target.maxAge = maxAge + target.effect = effect.clone() + target.textureSheet = textureSheet + target.speed = speed + target.sign = sign + target.speedLimit = speedLimit + target.light = light + target.yaw = yaw + target.pitch = pitch + target.roll = roll + target.cameraOption = cameraOption + target.axis = axis + } +} 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..f0dc42c8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt @@ -0,0 +1,144 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.SerializableData +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.display.DisplayEntityManager +import cn.coostack.cooparticlesapi.display.DisplayEntity +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import io.netty.buffer.Unpooled +import net.minecraft.core.RegistryAccess +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * SerializableData wrapper for DisplayEntity emitters entry. + */ +class DisplayEntityEmittersData( + var entityType: String = "", + var entityData: ByteArray = ByteArray(0), + var visibleRange: Float = 256f +) : SerializableData { + constructor(entity: DisplayEntity) : this() { + setEntity(entity) + } + + companion object { + @JvmStatic + val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, data -> + buf.writeUtf(data.entityType) + buf.writeInt(data.entityData.size) + buf.writeBytes(data.entityData) + buf.writeFloat(data.visibleRange) + }, + { buf -> + val type = buf.readUtf() + val size = buf.readInt() + val bytes = ByteArray(size) + buf.readBytes(bytes) + val visibleRange = buf.readFloat() + DisplayEntityEmittersData(type, bytes, visibleRange).also { + it.registryAccess = buf.registryAccess() + } + } + ) + + @JvmStatic + fun fromEntity(entity: DisplayEntity): DisplayEntityEmittersData { + val registryAccess = entity.world?.registryAccess() ?: registryAccessFallback() + val buf = PacketByteBuf(Unpooled.buffer(), registryAccess) + entity.getCodec().encode(buf, entity) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + return DisplayEntityEmittersData(entity::class.java.name, data).also { + it.registryAccess = registryAccess + } + } + + private fun registryAccessFallback(): RegistryAccess { + return CooParticlesAPI.registryAccessOrNull + ?: RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY) + } + } + + private var template: DisplayEntity? = null + private var preparedControler: DisplayEntity? = null + private var registryAccess: RegistryAccess? = null + + private fun decodeEntity(registryAccess: RegistryAccess): DisplayEntity { + val codec = DisplayEntityManager.registeredTypes[entityType] + ?: throw IllegalStateException("DisplayEntity codec not registered for type: $entityType") + return codec.decode( + PacketByteBuf(Unpooled.wrappedBuffer(entityData.copyOf()), registryAccess) + ) + } + + private fun cloneEntity(entity: DisplayEntity): DisplayEntity { + val type = entity::class.java + val ins = runCatching { + type.getDeclaredConstructor(Vec3::class.java, Level::class.java) + .apply { isAccessible = true } + .newInstance(entity.pos, entity.world) + }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() + + return ins.apply { update(entity) } + } + + private fun resolveEntity(registryAccess: RegistryAccess? = null): DisplayEntity { + return template ?: decodeEntity( + registryAccess ?: this.registryAccess ?: registryAccessFallback() + ).also { template = it } + } + + private fun createEntity(registryAccess: RegistryAccess? = null): DisplayEntity { + return cloneEntity(resolveEntity(registryAccess)) + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return PACKET_CODEC + } + + override fun clone(): SerializableData { + val entity = runCatching { cloneEntity(resolveEntity()) }.getOrNull() + return if (entity != null) { + DisplayEntityEmittersData(entity).also { it.visibleRange = visibleRange } + } else { + DisplayEntityEmittersData(entityType, entityData.copyOf(), visibleRange) + } + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + val entity = createEntity(world.registryAccess()) + entity.world = world + entity.pos = pos + preparedControler = entity + return entity + } + + override fun getDisplayer(): ParticleDisplayer { + val entity = preparedControler ?: createEntity().also { preparedControler = it } + preparedControler = null + return ParticleDisplayer.withDisplayEntity(entity) + } + + fun setEntity(entity: DisplayEntity): DisplayEntityEmittersData { + val clone = cloneEntity(entity) + val data = fromEntity(clone) + entityType = data.entityType + entityData = data.entityData + template = clone + preparedControler = null + return this + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt new file mode 100644 index 00000000..9f8cf37b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt @@ -0,0 +1,5 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +fun interface ParticleDataFactory { + fun create(): ControlableParticleData +} \ No newline at end of file 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..052327c8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt @@ -0,0 +1,122 @@ +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.network.PacketByteBuf + +import net.minecraft.server.level.ServerLevel +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + + +/** + * 发射单个粒子的 粒子发射器 + * + * 单个粒子通过 ParticleEmitters 修改粒子参数 + * + * -> 不固定数量的ParticleStyleData + * -> 方便输入的 + */ +interface ParticleEmitters : ServerControler, NetworkDirtyMarkable { + var pos: Vec3 + var world: Level? + var tick: Int + + /** + * 当maxTick == -1时 + * 代表此粒子不会由生命周期控制 + */ + var maxTick: Int + var delay: Int + var uuid: UUID + var canceled: Boolean + var playing: Boolean + + /** + * @param innerClass 是否在类内添加 + * 在类内添加的event handler不会参与codec传输 + */ + 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) + + /** + * 注册时传入的CODEC必须和这个完全一致 + * + * 编解码器 + * + * 编码粒子信息, 当前位置 + */ + fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + + override fun getValue(): ParticleEmitters { + return this + } + + /** + * 标记发射器的完整网络状态需要更新。 + * + * Manager 会在当前服务端 tick 结束时合并发送,同一发射器一 tick 内只会更新一次。 + */ + 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..1e0a0eff --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt @@ -0,0 +1,398 @@ +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.network.PacketByteBuf + +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 { + // 已经start的 emitters + val emittersCodec = HashMap>() + + /** + * 服务器拥有 + */ + val serverEmitters = HashMap() + internal val visible = ConcurrentHashMap>() + private val dirtyEmitters = ConcurrentHashMap.newKeySet() + + /** + * 客户端可视 + */ + val clientEmitters = ConcurrentHashMap() + + /** 返回客户端当前可见 Emitter 数。 */ + fun clientEmitterCount(): Int = clientEmitters.size + + /** 返回服务端当前 Emitter 数。 */ + fun serverEmitterCount(): Int = serverEmitters.size + + fun getCodecFromID(id: String): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec? { + return emittersCodec[id] + } + + + /** + * 在客户端执行 + */ + @JvmStatic + fun register( + id: String, + codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + ): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + 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 + } + + /** + * 玩家离开服务器时清理其 Emitter 可见缓存,使重连后重新发送创建包。 + * + * @param player 已断开连接的服务端玩家 + */ + 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 registryAccess = emitters.world?.registryAccess() ?: CooParticlesAPI.registryAccessOrNull ?: return ByteArray(0) + val buf = PacketByteBuf( + Unpooled.buffer(), + registryAccess + ) + 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 + } + // 获取instance + 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/PhysicConstant.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt new file mode 100644 index 00000000..7c9e5981 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt @@ -0,0 +1,23 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +object PhysicConstant { + /** + * 时间单位 tick + */ + const val EARTH_GRAVITY = 0.05 + + /** + * 海平面空气密度 + */ + const val SEA_AIR_DENSITY = 1.225 + + /** + * 粒子物理拖动系数 + */ + const val DRAG_COEFFICIENT = 0.01 + + /** + * 粒子横截面积 + */ + const val CROSS_SECTIONAL_AREA = 0.01 +} \ No newline at end of file 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..fb24d55d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt @@ -0,0 +1,270 @@ +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 + +/** + * 防止老是要手写等 一堆莫名其妙的随机数据而设立的 + * @param maxAge 粒子最大生命周期 + * @param minAge 粒子最小生命周期 + * @param maxCount 粒子最大个数 + * @param minCount 粒子最小个数 + * @param maxSize 粒子最大尺寸 + * @param minSize 粒子最小尺寸 + * @param maxSpeed 粒子最大速度 + * @param minSpeed 粒子最小速度 + * + */ +class SimpleRandomParticleData { + + companion object { + val PACKET_CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.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..8607fd24 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt @@ -0,0 +1,493 @@ +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 + +/** + * 只生成 [ControlableCParticleData] 的可变换 GPU 粒子发射器。 + * + * 每个实例拥有独立的 GPU 坐标空间。[space] 为 [CParticleEmitterSpace.LOCAL] 时,已经生成的粒子会随 + * 发射器一起平移、按 [emitterRotation] 旋转并等比缩放;[particleRotation] 只控制新粒子的出生方向。 + * 切换到 [CParticleEmitterSpace.WORLD] 后,新粒子改用世界坐标, + * 行为与 [ClassParticleEmitters] 的 GPU 路径一致。两类粒子使用不同 system,切换模式不会移动旧粒子。 + * + * 本类型直接实现 [ParticleEmitters],不继承 [ClassParticleEmitters]。逐粒子控制器、粒子事件和死亡重生 + * 不属于 GPU system 能力,因此不提供 CPU 回退。 + * + * @param pos 发射器世界坐标 + * @param world 发射器所在世界 + */ +abstract class TransformableCParticleEmitter( + pos: Vec3, + override var world: Level?, +) : ParticleEmitters { + + /** 发射器当前的世界坐标;直接修改后最迟在下一次 tick 同步。 */ + private val posState = dirty(pos) + override var pos by posState + + /** 当前生命周期 tick。 */ + override var tick: Int = 0 + + /** 生命周期上限;`-1` 表示不按 tick 自动结束。 */ + override var maxTick: Int = 120 + + /** 两次发射之间的 tick 间隔;小于 `1` 时按每 tick 发射处理。 */ + override var delay: Int = 0 + + /** 跨服务端和客户端识别同一发射器的 UUID。 */ + override var uuid: UUID = UUID.randomUUID() + + /** 是否已经结束生命周期。 */ + override var canceled: Boolean = false + + /** 是否已经启动。 */ + override var playing: Boolean = false + + /** 新粒子使用的坐标空间。切换不会改变已经生成的粒子。 */ + var space: CParticleEmitterSpace = CParticleEmitterSpace.LOCAL + + /** LOCAL 粒子所在的发射器空间旋转;变化时会带动已经生成的粒子。 */ + var emitterRotation: Quaternionf = Quaternionf() + set(value) { + require(value.isFinite && value.lengthSquared() > 1e-12F) { + "rotation must be finite and non-zero" + } + field = Quaternionf(value).normalize() + } + + /** 新生成粒子的出生位置和初速度旋转;不会改变已经生成的粒子。 */ + var particleRotation: Quaternionf = Quaternionf() + set(value) { + require(value.isFinite && value.lengthSquared() > 1e-12F) { + "particle rotation must be finite and non-zero" + } + field = Quaternionf(value).normalize() + } + + /** 发射器局部空间的等比缩放。必须为正数,保证 GPU 模拟可以换算回局部空间。 */ + var scale: Double = 1.0 + set(value) { + require(value.isFinite() && value > 0.0) { "scale must be finite and positive" } + field = value + } + + /** 环境空气密度,用于生成 GPU 阻力。 */ + var airDensity: Double = 0.0 + + /** 每 tick 向世界 Y 负方向施加的重力加速度。 */ + var gravity: Double = 0.0 + + /** 与 [ClassParticleEmitters] 保持一致的质量数据;当前 GPU 力场不会读取它。 */ + var mass: Double = 1.0 + + /** 风力方向。GPU 路径只支持非 relative 的全局风。 */ + var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { it.loadEmitters(this) } + + /** 是否沿发射器上一 tick 到当前 tick 的路径插值生成。 */ + var enableInterpolator: Boolean = false + + /** 发射路径插值器。 */ + var emittersInterpolator: Interpolator = LineEmitterInterpolator().setRefiner(5.0) + + /** 发射器在当前 tick 中产生的世界坐标位移。 */ + var emitterVelocity: Vec3 = Vec3.ZERO + private set + + private var lastTickPos: Vec3 = pos + private var lastSyncedTransform = TransformSnapshot.capture(this) + + /** + * 旧版 GPU 力场列表接口。 + * + * 该类不继承 [ClassParticleEmitters],因此单独保留旧签名。 + * 默认实现由流式接口转发,旧 emitter 覆写此方法仍能参与 GPU 模拟。 + * + * @return 按执行顺序排列的力场列表 + */ + open fun cparticleForces(): List = emptyList() + + /** + * 返回当前 GPU system 使用的附加力场。 + * + * 位置和方向参数都沿用 [ClassParticleEmitters] 的世界坐标语义。 + * 默认实现兼容旧版 [cparticleForces];新 emitter 可直接提交到 sink。 + * + * @param sink 当前 emitter 的力场快照 + */ + open fun submitCParticleForces(sink: CParticleForceSink) { + sink.submitAll(cparticleForces()) + } + + /** 返回方块碰撞网格相对当前发射器中心的保证范围。 */ + open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE + + /** + * 拒绝注册逐粒子事件。 + * + * @param handler 待注册事件处理器 + * @param innerClass 是否为实现类内部处理器 + * @throws UnsupportedOperationException 本发射器没有逐粒子 CPU 控制器 + */ + final override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + throw UnsupportedOperationException("TransformableCParticleEmitter does not support per-particle events") + } + + override fun start() { + if (playing) return + playing = true + lastTickPos = pos + emitterVelocity = Vec3.ZERO + lastSyncedTransform = TransformSnapshot.capture(this) + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + override fun stop() { + canceled = true + } + + override fun tick() { + if (canceled || !playing) return + val level = world ?: return + doTick() + emitterVelocity = pos - lastTickPos + lastTickPos = pos + + if (!level.isClientSide) { + syncServerTransformIfChanged() + increaseTick() + return + } + + TransformableCParticleEmitterBridge.syncSystems(this) + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + if (tick % max(1, delay) == 0) { + if (enableInterpolator) { + val refined = emittersInterpolator.getRefinedResult() + val denominator = (refined.size - 1).coerceAtLeast(1).toFloat() + refined.forEachIndexed { index, location -> + val spawnPos = location.toVector() + val lerpProgress = if (refined.size == 1) 1F else index / denominator + doSubtick(spawnPos, lerpProgress) + spawnParticle(spawnPos, lerpProgress) + } + } else { + spawnParticle(pos, 1F) + } + } + increaseTick() + } + + override fun spawnParticle(pos: Vec3, lerpProgress: Float) { + val level = world as? ClientLevel ?: return + val player = Minecraft.getInstance().player ?: return + val particles = genParticles(lerpProgress) + val batchSize = particles.size + particles.forEach { (data, relative) -> + val worldPosition = TransformableCParticleEmitterBridge.resolveWorldPosition( + this, + pos, + relative, + ) ?: return@forEach + if (player.position().distanceTo(worldPosition) > data.visibleRange) return@forEach + TransformableCParticleEmitterBridge.trySpawn( + this, + level, + pos, + relative, + data, + batchSize, + ) + } + } + + /** + * 服务器和客户端每 tick 都会调用,用于更新发射器自身状态。 + * + * 实现必须保证两端执行结果可同步,不能在这里直接访问仅客户端存在的渲染对象。 + */ + abstract fun doTick() + + /** + * 生成本次发射的 GPU 粒子及其相对位置。 + * + * LOCAL 模式下相对位置属于发射器坐标空间;WORLD 模式下会直接加到生成世界坐标。 + * + * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` + * @return 本次要生成的 GPU data 和位置 + */ + abstract fun genParticles(lerpProgress: Float): List> + + /** + * 在路径插值中的一次发射前调用。 + * + * @param current 本次插值后的发射世界坐标 + * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` + */ + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + /** + * 追加发射器空间旋转。 + * + * LOCAL 模式下,已经生成的粒子位置和速度都会跟随整个空间旋转。 + * + * @param rotation 发射器空间的增量四元数 + */ + fun rotateEmitter(rotation: Quaternionfc) { + emitterRotation = Quaternionf(emitterRotation).mul(rotation) + syncClientTransform() + } + + /** 绕发射器局部 Z 轴追加旋转。 */ + fun rotateEmitter(radian: Double) { + rotateEmitter(Quaternionf().rotateLocalZ(radian.toFloat())) + } + + /** + * 追加新粒子的出生旋转。 + * + * 只改变后续新粒子的出生位置和初速度;已经生成的粒子保持生成时的运动方向。 + * 粒子四边形朝向仍只由 `cameraOption` 和粒子自身旋转字段决定。 + * + * @param rotation 新粒子出生坐标系的增量四元数 + */ + fun rotateParticle(rotation: Quaternionfc) { + particleRotation = Quaternionf(particleRotation).mul(rotation) + syncClientTransform() + } + + /** 绕新粒子出生坐标系的局部 Z 轴追加旋转。 */ + fun rotateParticle(radian: Double) { + rotateParticle(Quaternionf().rotateLocalZ(radian.toFloat())) + } + + /** + * 按世界坐标增量移动发射器。 + * + * @param offset 本次增加的世界坐标位移 + */ + fun translate(offset: Vec3) { + teleportTo(pos + offset) + } + + /** + * 按世界坐标分量增量移动发射器。 + * + * @param x X 轴增量 + * @param y Y 轴增量 + * @param z Z 轴增量 + */ + fun translate(x: Double, y: Double, z: Double) { + translate(Vec3(x, y, z)) + } + + /** + * 设置绝对等比缩放。 + * + * @param new 大于零的缩放值 + */ + fun scale(new: Double) { + scale = new + syncClientTransform() + } + + /** + * 按倍率增量缩放。 + * + * @param factor 大于零的缩放倍率 + */ + fun scaleBy(factor: Double) { + require(factor.isFinite() && factor > 0.0) { "scale factor must be finite and positive" } + scale(scale * factor) + } + + override fun teleportTo(to: Vec3) { + pos = to + syncClientTransform() + } + + override fun rotateToPoint(to: RelativeLocation) { + val next = Quaternionf() + Math3DUtil.rotateQuatToPoint(next, to) + emitterRotation = next + syncClientTransform() + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + val next = Quaternionf() + Math3DUtil.rotateQuatToPoint(next, to) + next.rotateLocalZ(radian.toFloat()) + emitterRotation = next + syncClientTransform() + } + + override fun rotateAsAxis(radian: Double) { + rotateEmitter(radian) + } + + override fun update(emitters: ParticleEmitters) { + if (emitters !is TransformableCParticleEmitter || getEmittersID() != emitters.getEmittersID()) return + posState.setCodecValue(emitters.pos) + world = world ?: emitters.world + tick = emitters.tick + maxTick = emitters.maxTick + delay = emitters.delay + uuid = emitters.uuid + canceled = emitters.canceled + playing = emitters.playing + space = emitters.space + emitterRotation = Quaternionf(emitters.emitterRotation) + particleRotation = Quaternionf(emitters.particleRotation) + scale = emitters.scale + gravity = emitters.gravity + airDensity = emitters.airDensity + mass = emitters.mass + wind = emitters.wind.also { it.loadEmitters(this) } + enableInterpolator = emitters.enableInterpolator + emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) + ParticleEmittersRegistryHelper.updateEmitter(this, emitters) + resetRuntimeState(addInterpolatorPoint = false) + syncClientTransform() + } + + /** 结束后停止接收新粒子,并在已有粒子死亡后释放专属 GPU systems。 */ + internal fun finishClientSystems() { + TransformableCParticleEmitterBridge.finishEmitter(uuid) + } + + private fun syncClientTransform() { + if (world?.isClientSide == true) { + TransformableCParticleEmitterBridge.syncSystems(this) + } + } + + private fun syncServerTransformIfChanged() { + val current = TransformSnapshot.capture(this) + if (current == lastSyncedTransform) return + markDirty() + lastSyncedTransform = current + } + + private fun increaseTick() { + if (++tick >= maxTick && maxTick != -1) { + stop() + } + } + + private fun resetRuntimeState(addInterpolatorPoint: Boolean) { + lastTickPos = pos + emitterVelocity = Vec3.ZERO + lastSyncedTransform = TransformSnapshot.capture(this) + if (addInterpolatorPoint && enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + private data class TransformSnapshot( + val pos: Vec3, + val rotationX: Float, + val rotationY: Float, + val rotationZ: Float, + val rotationW: Float, + val particleRotationX: Float, + val particleRotationY: Float, + val particleRotationZ: Float, + val particleRotationW: Float, + val scale: Double, + val space: CParticleEmitterSpace, + ) { + companion object { + fun capture(emitter: TransformableCParticleEmitter): TransformSnapshot { + val rotation = emitter.emitterRotation + return TransformSnapshot( + emitter.pos, + rotation.x, + rotation.y, + rotation.z, + rotation.w, + emitter.particleRotation.x, + emitter.particleRotation.y, + emitter.particleRotation.z, + emitter.particleRotation.w, + emitter.scale, + emitter.space, + ) + } + } + } + + companion object { + /** 编码所有可变换 GPU emitter 共用的网络字段。 */ + fun encodeBase(data: TransformableCParticleEmitter, buf: PacketByteBuf) { + 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.writeByte(data.space.networkId) + buf.writeQuaternion(data.emitterRotation) + buf.writeQuaternion(data.particleRotation) + buf.writeDouble(data.scale) + 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) + } + + /** 解码所有可变换 GPU emitter 共用的网络字段。 */ + fun decodeBase(container: TransformableCParticleEmitter, buf: PacketByteBuf) { + container.posState.setCodecValue(buf.readVec3()) + container.tick = buf.readInt() + container.maxTick = buf.readInt() + container.delay = buf.readInt() + container.uuid = buf.readUUID() + container.canceled = buf.readBoolean() + container.playing = buf.readBoolean() + container.space = CParticleEmitterSpace.fromNetworkId(buf.readUnsignedByte().toInt()) + container.emitterRotation = buf.readQuaternion() + container.particleRotation = buf.readQuaternion() + container.scale = buf.readDouble() + container.gravity = buf.readDouble() + container.airDensity = buf.readDouble() + container.mass = buf.readDouble() + container.enableInterpolator = buf.readBoolean() + container.emittersInterpolator.setRefiner(buf.readDouble()) + val windId = buf.readUtf() + container.wind = WindDirections.getCodecFromID(windId).decode(buf).also { + it.loadEmitters(container) + } + container.resetRuntimeState(addInterpolatorPoint = true) + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt new file mode 100644 index 00000000..91991286 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt @@ -0,0 +1,30 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class OrbitMode { + /** + * 物理轨道(默认) + * + * - 切向 + 径向纠正 + * - 手感最真实 + * - 需要限幅,否则启动阶段可能不稳定 + */ + PHYSICAL, + + /** + * 弹簧轨道 + * + * - 半径误差像弹簧一样回弹 + * - 非常稳定,不容易飞走 + * - 手感偏“设计感” + */ + SPRING, + + /** + * 吸附轨道 + * + * - 每 tick 直接把粒子拉到目标半径附近 + * - 最稳定,最不物理 + * - 适合魔法阵 / UI / 装饰粒子 + */ + SNAP +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt new file mode 100644 index 00000000..eeee7078 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt @@ -0,0 +1,128 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.max + +/** + * 吸引/排斥力(指向目标点的加速度) + * + * - strength > 0:吸引(往目标点拉) + * - strength < 0:排斥(从目标点推开) + * - falloff 决定“近强远弱”的衰减方式 + */ +class ParticleAttractionCommand() : ParticleCommand { + + /** 目标点(世界坐标)。 */ + var target: Supplier = Supplier { Vec3.ZERO } + + /** + * 强度(每 tick 的速度增量基准)。 + * + * - 0.1 ~ 0.5:轻微牵引(像磁力) + * - 0.5 ~ 3.0:明显吸附(像黑洞) + * - 3.0+:非常暴力(需要配合 speedLimit / damping) + * + * 负数则变成排斥力。 + */ + var strength: Double = 0.8 + + /** + * 有效范围尺度(>0)。 + * + * - 越大:衰减越慢,远处也能感受到吸力 + * - 越小:只有靠近目标才明显 + */ + var range: Double = 8.0 + + /** + * 衰减幂次(>=1)。 + * + * - 1:衰减较缓 + * - 2:更像“平方反比”趋势 + * - 3:近距离极强,远处几乎无 + */ + var falloffPower: Double = 2.0 + + /** + * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 + * + * - 建议 0.1 ~ 1.0 + */ + var minDistance: Double = 0.25 + + /** + * @param target 目标点(世界坐标) + * @param strength 强度(>0吸引 <0排斥) + * @param range 范围尺度,越大影响越远 + * @param falloffPower 衰减幂次(>=1) + * @param minDistance 最小距离钳制,防止爆炸/NaN + */ + constructor( + target: Supplier = Supplier { Vec3.ZERO }, + strength: Double = 0.8, + range: Double = 8.0, + falloffPower: Double = 2.0, + minDistance: Double = 0.25, + ) : this() { + this.target = target + this.strength = strength + this.range = range + this.falloffPower = falloffPower + this.minDistance = minDistance + } + + /** 目标点(世界坐标)。 */ + fun target(v: Supplier) = apply { target = v } + + /** + * 强度(每 tick 的速度增量基准)。 + * + * - 0.1 ~ 0.5:轻微牵引(像磁力) + * - 0.5 ~ 3.0:明显吸附(像黑洞) + * - 3.0+:非常暴力(需要配合 speedLimit / damping) + * + * 负数则变成排斥力。 + */ + fun strength(v: Double) = apply { strength = v } + + /** + * 有效范围尺度(>0)。 + * + * - 越大:衰减越慢,远处也能感受到吸力 + * - 越小:只有靠近目标才明显 + */ + fun range(v: Double) = apply { range = v } + + /** + * 衰减幂次。 + * + * - 1:衰减较缓 + * - 2:更像“平方反比”趋势 + * - 3:近距离极强,远处几乎无 + */ + fun falloffPower(v: Double) = apply { falloffPower = v } + + /** + * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 + * + * - 建议 0.1 ~ 1.0 + */ + fun minDistance(v: Double) = apply { minDistance = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val dir = target.get().subtract(pos) + var d = dir.length() + if (d < 1e-9) return + d = max(d, minDistance) + + val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) + val dv = dir.normalize().scale(strength * falloff) + + data.velocity = data.velocity.add(dv) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt new file mode 100644 index 00000000..74acb430 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle + + +fun interface ParticleCommand { + /** + * 粒子“命令/模块”接口:用于在粒子每个 tick 更新前,对粒子数据进行一次可组合的逻辑处理。 + * + * ## 设计目的 + * 你可以把它理解成 **Unity 粒子系统的一个 Module**(例如:旋转力、阻尼、吸引力、漩涡、流场等): + * - 每个 module 都实现一个 `execute(...)` + * - 在粒子 tick 之前按顺序调用这些 module + * - module 之间通过“修改同一个 data”来叠加效果(通常是 `data.velocity`、`data.yaw/pitch/roll` 等) + * + * ## 调用时机(强烈建议) + * 推荐在 `Emitter.singleParticleAction(...)` 中,将命令挂载到 `ParticleControler.addPreTickAction { ... }`: + * - 这样命令会在 **每个 tick** 都执行一次(而不是只在生成时执行一次) + * - 且执行时你能拿到 “实时的粒子实例状态”(`particle.loc`, `particle.onTheGround`, `particle.currentAge` 等) + * + * ## 参数说明 + * @param data + * 可变的粒子控制数据(你自己的控制层数据)。 + * - **常见修改点**:`data.velocity`(加速度/阻尼/流场)、`data.faceToCamera`、`data.yaw/pitch/roll` 等 + * - 注意:`ControlableParticleData` 中有一部分字段在粒子生成后只生效一次(例如颜色/纹理等), + * 因此命令更适合修改“每 tick 生效”的字段(通常是 velocity 与旋转等)。 + * + * @param particle + * 当前 tick 正在更新的粒子实例(客户端真实粒子对象)。 + * - **常用读取**:`particle.loc`(位置)、`particle.onTheGround`(是否落地)、`particle.currentAge`(年龄)等 + * - **注意**:一般不建议在命令里直接 teleport 粒子位置;更推荐通过 `data.velocity` 影响运动, + * 由发射器的 physics/move 逻辑统一处理。 + * + * ## 组合与顺序 + * 多个命令按添加顺序执行: + * - 前面的命令可能会影响后面的命令输入(例如先吸引再阻尼 vs 先阻尼再吸引) + * - 如果你在命令里做了 clamp/limit(例如限速),通常建议放到最后一个命令执行 + * + * ## 编写建议(实践约定) + * - **保持轻量**:命令每 tick 对每个粒子都会执行,避免昂贵计算/分配对象(尤其是大量粒子场景) + * - **不要产生 NaN**:对距离接近 0 的情况要做钳制(例如 minDistance) + * - **可复用**:命令实例最好可以被多个粒子共享(内部不要缓存“单粒子状态”) + */ + fun execute(data: ControlableParticleData, particle: ControlableParticle) +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt new file mode 100644 index 00000000..9ad00fcb --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt @@ -0,0 +1,120 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle + +/** + * `ParticleCommand` 的顺序队列:用于把多个“粒子模块/预设”组合成一个可复用的执行链。 + * + * ## 设计目的 + * 让你用非常接近 Unity 粒子系统 “模块叠加”的方式组织效果: + * - 每个 module 都是一个 `ParticleCommand` + * - 通过队列把多个 module 组装起来 + * - 每 tick 调一次 `applyVelocity(...)`,队列会按顺序把所有 module 应用到同一个粒子上 + * + * ## 典型使用方式 + * 在 `Emitter.singleParticleAction(...)` 里把队列挂到粒子 controller 的 preTick: + * ```kotlin + * val queue = ParticleCommandQueue() + * .add(AirDragCommand(0.15)) + * .add(AttractionCommand(target = center, strength = 0.8)) + * + * override fun singleParticleAction( + * controler: ParticleControler, + * data: ControlableParticleData, + * spawnPos: RelativeLocation, + * spawnWorld: Level, + * particleLerpProgress: Float, + * posLerpProgress: Float + * ) { + * controler.addPreTickAction { + * queue.applyVelocity(data, this) + * } + * } + * ``` + */ +class ParticleCommandQueue { + val commands = + ArrayDeque Boolean>>() + + @Suppress("UNCHECKED_CAST") + private val alwaysExecutePredicate = + AlwaysExecutePredicate as ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean + + /** + * 对当前粒子执行队列中的所有命令。 + * + * @param data 当前粒子的可变控制数据(被命令叠加修改) + * @param particle 当前 tick 正在更新的粒子实例(用于读取实时状态) + */ + fun applyVelocity(data: ControlableParticleData, particle: ControlableParticle) { + commands.forEach { + if (it.second(it.first, data, particle)) { + it.first.execute(data, particle) + } + } + } + + /** + * 更新队列中指定索引位置的 Command 数据。 + * + * @param T Command 的具体类型 + * @param applier 对该 Command 执行的更新逻辑(直接修改其内部参数) + * @param index Command 在队列中的索引位置 + */ + fun updateWith(index: Int, applier: T.() -> Unit): ParticleCommandQueue = + apply { + if (index !in commands.indices) { + return@apply + } + val command = commands[index].first + runCatching { applier(command as T) } + } + + /** + * 更新队列中所有符合指定类型的 Command。 + * + * @param T 需要被更新的 Command 类型 + * @param applier 对所有匹配 Command 执行的更新逻辑 + */ + inline fun updateWithTypes(applier: T.() -> Unit): ParticleCommandQueue = apply { + commands.map { it.first }.filterIsInstance() + .forEach { + applier(it) + } + } + + /** + * 向队列末尾追加一个命令,并返回自身以支持链式调用。 + * + * @param command 要追加的粒子命令 + * @return this(便于 `.add(...).add(...)` 链式写法) + */ + fun add(command: ParticleCommand): ParticleCommandQueue { + commands.add(command to alwaysExecutePredicate) + return this + } + + /** + * 向队列末尾追加一个命令,并返回自身以支持链式调用。 + * + * @param command 要追加的粒子命令 + * @param predicate 执行这个命令的条件 + * @return this(便于 `.add(...).add(...)` 链式写法) + */ + fun add( + command: ParticleCommand, + predicate: ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean + ): ParticleCommandQueue { + commands.add(command to predicate) + return this + } + + private object AlwaysExecutePredicate : Function3 { + override fun invoke( + command: ParticleCommand, + data: ControlableParticleData, + particle: Any? + ): Boolean = true + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt new file mode 100644 index 00000000..e7190ae1 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt @@ -0,0 +1,334 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.abs +import kotlin.math.floor + +class ParticleDistortionCommand() : ParticleCommand { + var center: Supplier = Supplier { Vec3.ZERO } + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + var radius: Double = 3.0 + var radialStrength: Double = 0.35 + var axialStrength: Double = 0.25 + var tangentialStrength: Double = 0.0 + var frequency: Double = 0.25 + var timeScale: Double = 0.1 + var phaseOffset: Double = 0.0 + var followStrength: Double = 0.35 + var maxStep: Double = 0.6 + var baseAxial: Double = 0.0 + var seedOffset: Int = 0 + var useLifeCurve: Boolean = false + + /** + * @param center 扰动中心点(轴线穿过的点,世界坐标)。 + * - 建议与发射器中心一致,这样“环”会被稳定地扭曲而不是整体漂移。 + * - 使用 Supplier 便于跟随移动的实体/点位(每 tick 取一次)。 + * + * @param axis 扰动轴(环所在平面的法向/法线方向)。 + * - 会自动 normalize,零向量会回退到 (0,1,0)。 + * - (0,1,0) 表示环在 XZ 平面;(1,0,0) 表示环在 YZ 平面。 + * + * @param radius 基础半径(未扰动前的目标环半径)。 + * - 设为发射器环半径时,扰动主要表现为“扭曲”而不是整体收缩/扩张。 + * - 过小会让环过紧,过大则偏离发射器形状。 + * + * @param radialStrength 径向扰动强度(沿“半径方向”的噪声幅度)。 + * - 控制“鼓包/凹陷”的程度,值越大环越不规则。 + * - 设为 0 表示不做径向扭曲,只保留其它方向扰动。 + * + * @param axialStrength 轴向扰动强度(沿“轴线方向”的噪声幅度)。 + * - 控制环在法向上的起伏(像波浪上下抖动)。 + * - 设为 0 则保持在同一平面内。 + * + * @param tangentialStrength 切向扰动强度(沿“绕环方向”的噪声幅度)。 + * - 会让粒子沿环方向滑移,产生“扭拧/拉扯”的感觉。 + * - 设为 0 可避免环周向被拉散。 + * + * @param frequency 空间频率(噪声随位置变化的密度)。 + * - 越大越“细碎/抖动”,越小越“平滑/大块”。 + * - 影响扭曲的纹理尺度。 + * + * @param timeScale 时间变化速度(噪声随时间滚动的快慢)。 + * - 0 表示静态扭曲;越大越“流动/抖动”。 + * - 过大可能出现闪烁感。 + * + * @param phaseOffset 相位偏移(用来错开不同发射器/批次的噪声)。 + * - 不改变强度,只改变噪声的“起点”。 + * + * @param followStrength 追随强度(粒子被拉回扭曲环的力度)。 + * - 越大环越“紧”,不易发散;越小越“松”,容易漂离形状。 + * + * @param maxStep 单 tick 最大修正步长(对速度增量做上限)。 + * - 防止拉回过猛导致抖动或穿透。 + * - <=0 表示不限制。 + * + * @param baseAxial 基础轴向偏移(整体沿轴线的固定偏移量)。 + * - 用于把整个环抬高/降低或制作“偏心”的扭曲面。 + * + * @param seedOffset 噪声种子偏移(同一效果的“变体”)。 + * - 相同 seedOffset 会得到相同扭曲纹理;不同值能做多种随机样式。 + * + * @param useLifeCurve 是否按生命周期衰减扰动强度。 + * - true:出生强、死亡弱,更自然。 + * - false:全生命周期强度恒定,更稳定。 + */ + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + radius: Double = 3.0, + radialStrength: Double = 0.35, + axialStrength: Double = 0.25, + tangentialStrength: Double = 0.0, + frequency: Double = 0.25, + timeScale: Double = 0.1, + phaseOffset: Double = 0.0, + followStrength: Double = 0.35, + maxStep: Double = 0.6, + baseAxial: Double = 0.0, + seedOffset: Int = 0, + useLifeCurve: Boolean = false, + ) : this() { + this.center = center + this.axis = axis + this.radius = radius + this.radialStrength = radialStrength + this.axialStrength = axialStrength + this.tangentialStrength = tangentialStrength + this.frequency = frequency + this.timeScale = timeScale + this.phaseOffset = phaseOffset + this.followStrength = followStrength + this.maxStep = maxStep + this.baseAxial = baseAxial + this.seedOffset = seedOffset + this.useLifeCurve = useLifeCurve + } + + /** + * 设置扰动中心点(轴线穿过的点,世界坐标)。 + * + * - 建议与发射器中心一致,保证环形整体不漂移。 + * - Supplier 可用于跟随移动对象,每 tick 自动读取最新位置。 + */ + fun center(v: Supplier) = apply { center = v } + + /** + * 设置扰动轴(环所在平面的法线方向)。 + * + * - 会在计算时 normalize。 + * - (0,1,0) 为 XZ 平面,(1,0,0) 为 YZ 平面。 + */ + fun axis(v: Vec3) = apply { axis = v } + + /** + * 设置基础半径(未扰动前的目标环半径)。 + * + * - 与发射器环半径一致时最稳定。 + * - 过小会让环“收紧”,过大可能偏离发射器形状。 + */ + fun radius(v: Double) = apply { radius = v } + + /** + * 设置径向扰动强度(沿半径方向的扭曲幅度)。 + * + * - 越大越“鼓包/凹陷”,环越不规则。 + * - 设为 0 可保持圆环半径稳定。 + */ + fun radialStrength(v: Double) = apply { radialStrength = v } + + /** + * 设置轴向扰动强度(沿轴线方向的起伏幅度)。 + * + * - 控制环在法向上的上下波动。 + * - 设为 0 则保持在同一平面内。 + */ + fun axialStrength(v: Double) = apply { axialStrength = v } + + /** + * 设置切向扰动强度(沿环方向的滑移幅度)。 + * + * - 让粒子沿环周向“拉扯/扭拧”。 + * - 过大可能导致环被拉散。 + */ + fun tangentialStrength(v: Double) = apply { tangentialStrength = v } + + /** + * 设置空间频率(噪声随位置变化的密度)。 + * + * - 越大越细碎、越抖;越小越平滑、越大块。 + * - 决定扭曲纹理的尺度。 + */ + fun frequency(v: Double) = apply { frequency = v } + + /** + * 设置时间变化速度(噪声滚动速度)。 + * + * - 0 表示静态扭曲。 + * - 越大越活跃,但过大容易“闪烁抖动”。 + */ + fun timeScale(v: Double) = apply { timeScale = v } + + /** + * 设置相位偏移(错开不同发射器/批次的噪声)。 + * + * - 不影响强度,只改变噪声起点。 + * - 用于避免多个环“同步抖动”。 + */ + fun phaseOffset(v: Double) = apply { phaseOffset = v } + + /** + * 设置追随强度(粒子被拉回扭曲环的力度)。 + * + * - 越大越“紧”,不易发散。 + * - 越小越“松”,允许更自由的漂移。 + */ + fun followStrength(v: Double) = apply { followStrength = v } + + /** + * 设置单 tick 最大修正步长(速度增量上限)。 + * + * - 防止拉回过猛导致抖动或穿透。 + * - <=0 表示不限制。 + */ + fun maxStep(v: Double) = apply { maxStep = v } + + /** + * 设置基础轴向偏移(整体沿轴线的固定偏移量)。 + * + * - 可把环整体抬高/降低。 + * - 配合轴向扰动可做“偏心/倾移”的扭曲环。 + */ + fun baseAxial(v: Double) = apply { baseAxial = v } + + /** + * 设置噪声种子偏移(同效果的随机变体)。 + * + * - 相同 seedOffset -> 相同扭曲纹理。 + * - 不同 seedOffset -> 不同随机外观。 + */ + fun seedOffset(v: Int) = apply { seedOffset = v } + + /** + * 设置是否按生命周期衰减扰动强度。 + * + * - true:出生强、死亡弱,更自然。 + * - false:全生命周期恒定,更稳定。 + */ + fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val centerPos = center.get() + val ax = safeNormalize(axis) + val pos = particle.loc + val r = pos.subtract(centerPos) + val axialComp = ax.scale(r.dot(ax)) + var radial = r.subtract(axialComp) + var radialLen = radial.length() + if (radialLen < 1e-9) { + radial = anyPerp(ax) + radialLen = 1.0 + } else { + radial = radial.scale(1.0 / radialLen) + } + + var tangent = ax.cross(radial) + val tLen = tangent.length() + tangent = if (tLen < 1e-9) anyPerp(ax) else tangent.scale(1.0 / tLen) + + val lifeT = if (particle.lifetime > 0) { + particle.currentAge.toDouble() / particle.lifetime.toDouble() + } else { + 0.0 + } + val lifeMul = if (useLifeCurve) (1.0 - lifeT).coerceIn(0.0, 1.0) else 1.0 + + val time = particle.currentAge.toDouble() * timeScale + phaseOffset + val local = pos.subtract(centerPos) + val p = local.scale(frequency).add(time, time * 0.7, time * 1.3) + val seed = particle.controlUUID.hashCode() + seedOffset + + val nr = noise(p, seed + 11) + val na = noise(p, seed + 23) + val nt = noise(p, seed + 37) + + val targetRadius = (radius + nr * radialStrength * lifeMul).coerceAtLeast(0.0) + val targetAxial = baseAxial + na * axialStrength * lifeMul + val targetTangential = nt * tangentialStrength * lifeMul + + val targetPos = centerPos + .add(ax.scale(targetAxial)) + .add(radial.scale(targetRadius)) + .add(tangent.scale(targetTangential)) + + var dv = targetPos.subtract(pos).scale(followStrength) + val dvLen = dv.length() + if (maxStep > 0.0 && dvLen > maxStep) { + dv = dv.scale(maxStep / dvLen) + } + data.velocity = data.velocity.add(dv) + } + + private fun safeNormalize(v: Vec3): Vec3 { + val len = v.length() + return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) + } + + private fun anyPerp(axis: Vec3): Vec3 { + val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) + val perp = axis.cross(ref) + val len = perp.length() + return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) + } + + private fun noise(p: Vec3, seed: Int): Double { + return valueNoise3(p, seed) * 2.0 - 1.0 + } + + private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) + private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t + + private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { + var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 + n = (n xor (n ushr 13)) * 1274126177 + n = n xor (n ushr 16) + return (n and 0x7fffffff).toDouble() / 2147483647.0 + } + + private fun valueNoise3(p: Vec3, seed: Int): Double { + val x0 = floor(p.x).toInt() + val y0 = floor(p.y).toInt() + val z0 = floor(p.z).toInt() + val fx = p.x - x0 + val fy = p.y - y0 + val fz = p.z - z0 + + val u = fade(fx) + val v = fade(fy) + val w = fade(fz) + + fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) + + val n000 = h(0, 0, 0) + val n100 = h(1, 0, 0) + val n010 = h(0, 1, 0) + val n110 = h(1, 1, 0) + val n001 = h(0, 0, 1) + val n101 = h(1, 0, 1) + val n011 = h(0, 1, 1) + val n111 = h(1, 1, 1) + + val nx00 = lerp(n000, n100, u) + val nx10 = lerp(n010, n110, u) + val nx01 = lerp(n001, n101, u) + val nx11 = lerp(n011, n111, u) + + val nxy0 = lerp(nx00, nx10, v) + val nxy1 = lerp(nx01, nx11, v) + + return lerp(nxy0, nxy1, w) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt new file mode 100644 index 00000000..e908304b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt @@ -0,0 +1,99 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 + +/** + * 空气阻尼(Drag) + * + * 每 tick 把速度乘上 (1 - drag),让粒子慢下来、更“黏”。 + */ +class ParticleDragCommand() : ParticleCommand { + /** + * 阻尼强度(>=0)。 + * + * - 0.0:完全不阻尼 + * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) + * - 0.2 ~ 1.0:明显刹车(碎屑、火花) + * - >1.0:非常快停(像在胶水里) + */ + var damping: Double = 0.15 + + /** + * 最小速度阈值(<=0 表示不使用)。 + * + * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 + */ + var minSpeed: Double = 0.0 + + /** + * 额外的线性阻力(每 tick 额外减去 speed * linear) + * + * - 推荐保持 0,除非你想要“低速更粘”的感觉 + */ + var linear: Double = 0.0 + + /** + * @param damping 阻尼强度(>=0),越大速度衰减越快 + * @param minSpeed 低于该速度则直接归零(<=0 不启用) + * @param linear 线性阻力项(建议 0) + */ + constructor( + damping: Double = 0.15, + minSpeed: Double = 0.0, + linear: Double = 0.0, + ) : this() { + this.damping = damping + this.minSpeed = minSpeed + this.linear = linear + } + + /** + * 阻尼强度(>=0)。 + * + * - 0.0:完全不阻尼 + * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) + * - 0.2 ~ 1.0:明显刹车(碎屑、火花) + * - >1.0:非常快停(像在胶水里) + */ + fun damping(v: Double) = apply { damping = v } + + /** + * 最小速度阈值(<=0 表示不使用)。 + * + * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 + */ + fun minSpeed(v: Double) = apply { minSpeed = v } + + /** + * 额外的线性阻力(每 tick 额外减去 speed * linear) + * + * - 推荐保持 0,除非你想要“低速更粘”的感觉 + */ + fun linear(v: Double) = apply { linear = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + var v = data.velocity + val speed = v.length() + if (minSpeed > 0 && speed <= minSpeed) { + data.velocity = Vec3.ZERO + return + } + + // exp damping + val factor = GraphMathHelper.expDampFactor(damping, 1.0) + v = v.scale(factor) + + // optional linear drag + if (linear > 0.0) { + val s2 = v.length() + if (s2 > 1e-9) { + val shrink = (1.0 - linear).coerceIn(0.0, 1.0) + v = v.scale(shrink) + } + } + data.velocity = v + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt new file mode 100644 index 00000000..b50e6fb9 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt @@ -0,0 +1,119 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import kotlin.math.cos +import kotlin.math.sin + +/** + * 流场:根据粒子位置/时间生成一个“风向量”,并叠加到速度里。 + * + * 这个实现是解析型流场(sin/cos),特点: + * - 计算非常快 + * - 通过 frequency / amplitude / timeScale 能做出很多“丝带/涡流”效果 + */ +class ParticleFlowFieldCommand() : ParticleCommand { + + /** + * 振幅(每 tick 的速度增量幅度) + * + * - 0.01 ~ 0.1:微风(烟雾漂移) + * - 0.1 ~ 0.8:明显流动(能看出纹理) + * - >0.8:很强(容易乱飞,需要阻尼/限速) + */ + var amplitude: Double = 0.15 + + /** + * 空间频率(越大纹理越密,越小越平缓) + * + * - 0.05 ~ 0.2:大尺度流场 + * - 0.2 ~ 1.0:中小尺度卷曲 + */ + var frequency: Double = 0.25 + + /** + * 时间缩放(控制流场随时间变化速度) + * + * - 0:静态流场(像固定风) + * - 0.02 ~ 0.2:缓慢变动 + * - >0.3:快速抖动/波动 + */ + var timeScale: Double = 0.06 + + /** + * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) + */ + var phaseOffset: Double = 0.0 + + /** + * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) + */ + var worldOffset: Vec3 = Vec3.ZERO + + constructor( + amplitude: Double = 0.15, + frequency: Double = 0.25, + timeScale: Double = 0.06, + phaseOffset: Double = 0.0, + worldOffset: Vec3 = Vec3.ZERO, + ) : this() { + this.amplitude = amplitude + this.frequency = frequency + this.timeScale = timeScale + this.phaseOffset = phaseOffset + this.worldOffset = worldOffset + } + + /** + * 振幅(每 tick 的速度增量幅度) + * + * - 0.01 ~ 0.1:微风(烟雾漂移) + * - 0.1 ~ 0.8:明显流动(能看出纹理) + * - >0.8:很强(容易乱飞,需要阻尼/限速) + */ + fun amplitude(v: Double) = apply { amplitude = v } + + /** + * 空间频率(越大纹理越密,越小越平缓) + * + * - 0.05 ~ 0.2:大尺度流场 + * - 0.2 ~ 1.0:中小尺度卷曲 + */ + fun frequency(v: Double) = apply { frequency = v } + + /** + * 时间缩放(控制流场随时间变化速度) + * + * - 0:静态流场(像固定风) + * - 0.02 ~ 0.2:缓慢变动 + * - >0.3:快速抖动/波动 + */ + fun timeScale(v: Double) = apply { timeScale = v } + + /** + * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) + */ + fun phaseOffset(v: Double) = apply { phaseOffset = v } + + /** + * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) + */ + fun worldOffset(v: Vec3) = apply { worldOffset = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val p = particle.loc.add(worldOffset) + val t = (particle.currentAge.toDouble() * timeScale) + phaseOffset + + // A cheap "curl-ish" analytic field + val fx = sin((p.y + t) * frequency) + cos((p.z - t) * frequency) + val fy = sin((p.z + t) * frequency) + cos((p.x + t) * frequency) + val fz = sin((p.x - t) * frequency) + cos((p.y - t) * frequency) + + // normalize-ish (avoid sqrt cost, keep stable magnitude) + val scale = 0.5 + val dv = Vec3(fx * scale, fy * scale, fz * scale).scale(amplitude) + + data.velocity = data.velocity.add(dv) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt new file mode 100644 index 00000000..7b73bd04 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt @@ -0,0 +1,20 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.particles.ControlableParticle + +/** + * 在emitter内部使用updatePhysics 或者使用这个 + * + * @property emitter 对应粒子发射器 + */ +class ParticleGravityCommand(val emitter: ClassParticleEmitters) : ParticleCommand { + override fun execute( + data: ControlableParticleData, + particle: ControlableParticle + ) { + data.velocity = data.velocity.add(0.0, -emitter.gravity, 0.0) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt new file mode 100644 index 00000000..7dd6b709 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class ParticleInheritMode { + INITIAL, + CURRENT +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt new file mode 100644 index 00000000..8c80e5e4 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt @@ -0,0 +1,340 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector3f +import java.util.function.Supplier +import kotlin.random.Random + +/** + * 继承速度命令(Inherit Velocity)。 + * + * 目标: + * - 让粒子继承“发射源速度”或任意自定义速度源 + * - 支持一次性继承(INITIAL)和持续跟随(CURRENT) + * - 支持按轴屏蔽、生命周期权重、空间转换、阻尼跟随 + * + * 常见场景: + * - 武器挥动拖尾:CURRENT + 适度 damping + * - 喷射火花:INITIAL + 较高 multiplier + */ +class ParticleInheritVelocityCommand() : ParticleCommand { + /** + * 速度来源。 + * + * 默认返回 `Vec3.ZERO`,通常你会传入: + * - `Supplier { emitter.emitterVelocity }` + * - 或任意自定义动态速度源 + */ + var source: Supplier = Supplier { Vec3.ZERO } + + /** + * 继承模式。 + * + * - INITIAL:只在首次执行时继承,后续保持(或按 damping 衰减) + * - CURRENT:每 tick 跟随最新来源速度 + */ + var mode: ParticleInheritMode = ParticleInheritMode.INITIAL + + /** + * 继承倍率。 + * + * - 1.0:等于来源速度 + * - <1.0:弱继承 + * - >1.0:强化继承 + */ + var multiplier: Double = 1.0 + + /** + * 轴向掩码(逐轴相乘)。 + * + * 示例: + * - (1,0,1):忽略 Y 轴继承 + * - (0,1,0):只继承竖直方向 + */ + var axisMask: Vec3 = Vec3(1.0, 1.0, 1.0) + + /** + * 生命周期权重曲线。 + * + * 采样值会乘到继承速度上: + * - 1 -> 全量继承 + * - 0 -> 不继承 + */ + var overLifetime: FloatCurve = ConstantFloatCurve(1.0) + + /** + * 阻尼强度(用于平滑/衰减继承速度)。 + * + * - 0:不平滑,立即跟随 + * - >0:指数平滑,值越大变化越柔和 + */ + var damping: Double = 0.0 + + /** + * 继承速度长度上限。 + * + * - <=0:不限制 + * - >0:超过则截断 + */ + var maxContributionSpeed: Double = 0.0 + + /** + * 继承向量所在空间。 + * + * - WORLD:按世界轴解释 + * - LOCAL:按粒子当前旋转解释后再转到世界空间 + */ + var space: ParticleMotionSpace = ParticleMotionSpace.WORLD + + /** + * 是否启用按粒子固定随机缩放。 + */ + var randomizePerParticle: Boolean = false + + /** + * 随机缩放最小值(按轴独立采样)。 + */ + var randomScaleMin: Double = 1.0 + + /** + * 随机缩放最大值(按轴独立采样)。 + */ + var randomScaleMax: Double = 1.0 + + /** + * 随机种子偏移。 + */ + var randomSeedOffset: Int = 0 + + private val stateKey = "inherit_velocity.state.${System.identityHashCode(this)}" + private val randomScaleKey = "inherit_velocity.random_scale.${System.identityHashCode(this)}" + + private data class InheritState( + var initialized: Boolean = false, + var applied: Vec3 = Vec3.ZERO + ) + + private data class AxisRandomScale( + val x: Double, + val y: Double, + val z: Double + ) + + /** + * @param source 速度来源 + * @param mode 继承模式(INITIAL/CURRENT) + * @param multiplier 继承倍率 + * @param axisMask 轴向掩码 + * @param overLifetime 生命周期权重曲线 + * @param damping 平滑阻尼 + * @param maxContributionSpeed 继承速度上限 + * @param space 继承空间(WORLD/LOCAL) + */ + constructor( + source: Supplier, + mode: ParticleInheritMode = ParticleInheritMode.INITIAL, + multiplier: Double = 1.0, + axisMask: Vec3 = Vec3(1.0, 1.0, 1.0), + overLifetime: FloatCurve = ConstantFloatCurve(1.0), + damping: Double = 0.0, + maxContributionSpeed: Double = 0.0, + space: ParticleMotionSpace = ParticleMotionSpace.WORLD + ) : this() { + this.source = source + this.mode = mode + this.multiplier = multiplier + this.axisMask = axisMask + this.overLifetime = overLifetime + this.damping = damping + this.maxContributionSpeed = maxContributionSpeed + this.space = space + } + + /** + * 便捷构造:直接继承指定发射器的每 tick 位移速度。 + */ + constructor(emitter: ClassParticleEmitters) : this( + source = Supplier { emitter.emitterVelocity } + ) + + /** + * 设置速度来源。 + */ + fun source(v: Supplier) = apply { source = v } + + /** + * 设置继承模式。 + * + * - INITIAL:一次继承 + * - CURRENT:持续跟随 + */ + fun mode(v: ParticleInheritMode) = apply { mode = v } + + /** + * 设置继承倍率。 + */ + fun multiplier(v: Double) = apply { multiplier = v } + + /** + * 设置轴向掩码。 + */ + fun axisMask(v: Vec3) = apply { axisMask = v } + + /** + * 设置生命周期权重曲线。 + */ + fun overLifetime(v: FloatCurve) = apply { overLifetime = v } + + /** + * 设置阻尼强度。 + */ + fun damping(v: Double) = apply { damping = v } + + /** + * 设置继承速度上限。 + */ + fun maxContributionSpeed(v: Double) = apply { maxContributionSpeed = v } + + /** + * 设置继承空间(WORLD/LOCAL)。 + */ + fun space(v: ParticleMotionSpace) = apply { space = v } + + /** + * 设置是否启用按粒子固定随机缩放。 + */ + fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } + + /** + * 设置随机缩放区间。 + */ + fun randomScale(min: Double, max: Double) = apply { + randomScaleMin = min + randomScaleMax = max + } + + /** + * 设置随机种子偏移。 + */ + fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } + + /** + * 每 tick 应用继承速度。 + */ + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val state = particle.controler.bufferedData.getOrPut(stateKey) { InheritState() } as InheritState + + val t = normalizedLifetime(particle) + val lifeMultiplier = overLifetime.sample(t) + val randomScale = resolveRandomScale(particle) + + var target = source.get() + target = applyMask(target) + target = scaleAxis(target, randomScale) + target = target.scale(multiplier * lifeMultiplier) + target = transformBySpace(target, space, particle) + target = clampLength(target, maxContributionSpeed) + + target = when (mode) { + ParticleInheritMode.INITIAL -> { + if (!state.initialized) { + state.initialized = true + target + } else if (damping > 0.0) { + state.applied.scale(GraphMathHelper.expDampFactor(damping, 1.0)) + } else { + state.applied + } + } + + ParticleInheritMode.CURRENT -> { + state.initialized = true + if (damping > 0.0) { + val follow = 1.0 - GraphMathHelper.expDampFactor(damping, 1.0) + state.applied.add(target.subtract(state.applied).scale(follow.coerceIn(0.0, 1.0))) + } else { + target + } + } + } + + val delta = target.subtract(state.applied) + if (delta.lengthSqr() > 1e-12) { + data.velocity = data.velocity.add(delta) + } + state.applied = target + } + + private fun applyMask(vec: Vec3): Vec3 { + return Vec3( + vec.x * axisMask.x, + vec.y * axisMask.y, + vec.z * axisMask.z + ) + } + + private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { + return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) + } + + private fun normalizedLifetime(particle: ControlableParticle): Double { + if (particle.lifetime <= 0) return 0.0 + return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) + } + + private fun transformBySpace( + vec: Vec3, + space: ParticleMotionSpace, + particle: ControlableParticle + ): Vec3 { + if (space == ParticleMotionSpace.WORLD) return vec + + val q = Quaternionf().rotateXYZ( + particle.currentPitch, + particle.currentYaw, + particle.currentRoll + ) + val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) + v.rotate(q) + return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) + } + + private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { + if (!randomizePerParticle) { + return AxisRandomScale(1.0, 1.0, 1.0) + } + val cached = particle.controler.bufferedData[randomScaleKey] + if (cached is AxisRandomScale) { + return cached + } + + val minScale = minOf(randomScaleMin, randomScaleMax) + val maxScale = maxOf(randomScaleMin, randomScaleMax) + val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) + val fixed = if (maxScale - minScale <= 1e-9) { + AxisRandomScale(minScale, minScale, minScale) + } else { + AxisRandomScale( + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale) + ) + } + particle.controler.bufferedData[randomScaleKey] = fixed + return fixed + } + + private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { + if (maxLen <= 0.0) return vec + val len = vec.length() + if (len <= 1e-9 || len <= maxLen) return vec + return vec.scale(maxLen / len) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt new file mode 100644 index 00000000..1fb7be06 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt @@ -0,0 +1,377 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector3f +import kotlin.random.Random + +/** + * 生命周期运动命令(Force over Lifetime + Velocity over Lifetime)。 + * + * 你可以把它理解为 Unity 里两个模块的组合版: + * - Force over Lifetime:每 tick 给 `data.velocity` 增加一段加速度 + * - Velocity over Lifetime:每 tick 按模式塑形速度(叠加/覆盖/乘法) + * + * 执行流程: + * 1. 计算生命周期进度 `t = currentAge / lifetime` + * 2. 采样 Force 曲线并叠加到当前速度 + * 3. 采样 Velocity 曲线并按 `velocityMode` 生效 + * 4. 可选:应用按粒子固定随机缩放(提升群体离散感) + */ +class ParticleLifetimeMotionCommand() : ParticleCommand { + /** + * Force 曲线 X 分量(每 tick 对 velocity.x 的增量)。 + * + * - 正值:向世界/局部 +X 推 + * - 负值:向世界/局部 -X 推 + */ + var forceX: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Force 曲线 Y 分量(每 tick 对 velocity.y 的增量)。 + * + * 常用于: + * - 上升烟雾:前期正值,后期逐渐趋近 0 + * - 下坠火星:前期小正值,后期转负值 + */ + var forceY: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Force 曲线 Z 分量(每 tick 对 velocity.z 的增量)。 + */ + var forceZ: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Velocity 曲线 X 分量。 + * + * 实际如何生效取决于 `velocityMode`: + * - ADD:视作附加速度曲线 + * - OVERRIDE:视作目标速度 + * - MULTIPLY:视作按轴倍率 + */ + var velocityX: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Velocity 曲线 Y 分量。 + */ + var velocityY: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Velocity 曲线 Z 分量。 + */ + var velocityZ: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Force 曲线所在空间。 + * + * - WORLD:曲线向量直接当世界方向 + * - LOCAL:曲线向量会按粒子当前 pitch/yaw/roll 旋转到世界方向 + */ + var forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD + + /** + * Velocity 曲线所在空间。 + * + * - WORLD:速度塑形不随粒子朝向变化 + * - LOCAL:速度塑形会跟随粒子当前朝向 + */ + var velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD + + /** + * Velocity 曲线作用模式。 + * + * - ADD:把曲线速度作为“附加项”并做增量追踪 + * - OVERRIDE:把当前速度拉向曲线速度 + * - MULTIPLY:按轴对当前速度做乘法 + */ + var velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD + + /** + * 是否启用“按粒子固定随机缩放”。 + * + * 开启后每个粒子只在首次执行时采样一次随机倍率, + * 后续 tick 重用同一倍率,保证轨迹连贯。 + */ + var randomizePerParticle: Boolean = false + + /** + * 随机缩放最小值(按轴独立采样)。 + */ + var randomScaleMin: Double = 1.0 + + /** + * 随机缩放最大值(按轴独立采样)。 + */ + var randomScaleMax: Double = 1.0 + + /** + * 随机种子偏移。 + * + * 在相同参数下改变该值,可快速得到另一套离散分布。 + */ + var randomSeedOffset: Int = 0 + + /** + * 限制 Velocity over Lifetime 在单 tick 的最大速度改变量。 + * + * - <= 0:不限制 + * - > 0:超过该值会被裁剪 + * + * 作用:抑制曲线突变导致的速度跳变或爆速。 + */ + var maxVelocityDeltaPerTick: Double = 0.0 + + private val velocityStateKey = "lifetime_motion.velocity_state.${System.identityHashCode(this)}" + private val randomScaleKey = "lifetime_motion.random_scale.${System.identityHashCode(this)}" + + private data class VelocityState(var appliedVelocity: Vec3 = Vec3.ZERO) + private data class AxisRandomScale(val x: Double, val y: Double, val z: Double) + + /** + * @param forceX Force 曲线 X + * @param forceY Force 曲线 Y + * @param forceZ Force 曲线 Z + * @param velocityX Velocity 曲线 X + * @param velocityY Velocity 曲线 Y + * @param velocityZ Velocity 曲线 Z + * @param forceSpace Force 所在空间 + * @param velocitySpace Velocity 所在空间 + * @param velocityMode Velocity 生效模式 + */ + constructor( + forceX: FloatCurve = ConstantFloatCurve(0.0), + forceY: FloatCurve = ConstantFloatCurve(0.0), + forceZ: FloatCurve = ConstantFloatCurve(0.0), + velocityX: FloatCurve = ConstantFloatCurve(0.0), + velocityY: FloatCurve = ConstantFloatCurve(0.0), + velocityZ: FloatCurve = ConstantFloatCurve(0.0), + forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, + velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, + velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD + ) : this() { + this.forceX = forceX + this.forceY = forceY + this.forceZ = forceZ + this.velocityX = velocityX + this.velocityY = velocityY + this.velocityZ = velocityZ + this.forceSpace = forceSpace + this.velocitySpace = velocitySpace + this.velocityMode = velocityMode + } + + /** + * 一次性设置 Force 曲线 XYZ。 + * + * 示例: + * - `x=0,y>0,z=0`:纯上升 + * - `x/z 对称`:横向扩散 + */ + fun forceCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { + forceX = x + forceY = y + forceZ = z + } + + /** + * 一次性设置 Velocity 曲线 XYZ。 + * + * 建议: + * - 做“轨道化速度”用 `OVERRIDE` + * - 做“微调补偿”用 `ADD` + */ + fun velocityCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { + velocityX = x + velocityY = y + velocityZ = z + } + + /** + * 设置 Force 曲线空间。 + * + * - WORLD:固定世界方向 + * - LOCAL:跟随粒子朝向 + */ + fun forceSpace(v: ParticleMotionSpace) = apply { forceSpace = v } + + /** + * 设置 Velocity 曲线空间。 + * + * - WORLD:常用于全局风场/轨道 + * - LOCAL:常用于“朝前推进”类效果 + */ + fun velocitySpace(v: ParticleMotionSpace) = apply { velocitySpace = v } + + /** + * 设置 Velocity 曲线作用模式。 + * + * - ADD / OVERRIDE / MULTIPLY + */ + fun velocityMode(v: ParticleLifetimeVelocityMode) = apply { velocityMode = v } + + /** + * 设置是否启用按粒子固定随机缩放。 + */ + fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } + + /** + * 设置随机缩放区间。 + * + * - 当 `min == max` 时等价于固定倍率 + * - 区间越大,离散感越强,但整体可控性会下降 + */ + fun randomScale(min: Double, max: Double) = apply { + randomScaleMin = min + randomScaleMax = max + } + + /** + * 设置随机种子偏移。 + */ + fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } + + /** + * 设置 Velocity 曲线每 tick 最大速度改变量。 + */ + fun maxVelocityDeltaPerTick(v: Double) = apply { maxVelocityDeltaPerTick = v } + + /** + * 每 tick 应用生命周期运动。 + */ + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val t = normalizedLifetime(particle) + val randomScale = resolveRandomScale(particle) + + val force = transformBySpace( + scaleAxis(sampleForce(t), randomScale), + forceSpace, + particle + ) + if (force.lengthSqr() > 1e-12) { + data.velocity = data.velocity.add(force) + } + + val sampledVelocity = transformBySpace( + scaleAxis(sampleVelocity(t), randomScale), + velocitySpace, + particle + ) + when (velocityMode) { + ParticleLifetimeVelocityMode.ADD -> applyVelocityAdditive(data, particle, sampledVelocity) + ParticleLifetimeVelocityMode.OVERRIDE -> { + particle.controler.bufferedData.remove(velocityStateKey) + var target = sampledVelocity + if (maxVelocityDeltaPerTick > 0.0) { + val delta = clampLength(target.subtract(data.velocity), maxVelocityDeltaPerTick) + target = data.velocity.add(delta) + } + data.velocity = target + } + + ParticleLifetimeVelocityMode.MULTIPLY -> { + particle.controler.bufferedData.remove(velocityStateKey) + val current = data.velocity + var target = Vec3( + current.x * sampledVelocity.x, + current.y * sampledVelocity.y, + current.z * sampledVelocity.z + ) + if (maxVelocityDeltaPerTick > 0.0) { + val delta = clampLength(target.subtract(current), maxVelocityDeltaPerTick) + target = current.add(delta) + } + data.velocity = target + } + } + } + + private fun applyVelocityAdditive( + data: ControlableParticleData, + particle: ControlableParticle, + sampledVelocity: Vec3 + ) { + val state = particle.controler.bufferedData.getOrPut(velocityStateKey) { VelocityState() } as VelocityState + val delta = clampLength(sampledVelocity.subtract(state.appliedVelocity), maxVelocityDeltaPerTick) + if (delta.lengthSqr() > 1e-12) { + data.velocity = data.velocity.add(delta) + } + state.appliedVelocity = state.appliedVelocity.add(delta) + } + + private fun sampleForce(t: Double): Vec3 { + return Vec3( + forceX.sample(t), + forceY.sample(t), + forceZ.sample(t) + ) + } + + private fun sampleVelocity(t: Double): Vec3 { + return Vec3( + velocityX.sample(t), + velocityY.sample(t), + velocityZ.sample(t) + ) + } + + private fun normalizedLifetime(particle: ControlableParticle): Double { + if (particle.lifetime <= 0) return 0.0 + return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) + } + + private fun transformBySpace( + vec: Vec3, + space: ParticleMotionSpace, + particle: ControlableParticle + ): Vec3 { + if (space == ParticleMotionSpace.WORLD) return vec + + val q = Quaternionf().rotateXYZ( + particle.currentPitch, + particle.currentYaw, + particle.currentRoll + ) + val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) + v.rotate(q) + return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) + } + + private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { + return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) + } + + private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { + if (!randomizePerParticle) { + return AxisRandomScale(1.0, 1.0, 1.0) + } + val cached = particle.controler.bufferedData[randomScaleKey] + if (cached is AxisRandomScale) { + return cached + } + val minScale = minOf(randomScaleMin, randomScaleMax) + val maxScale = maxOf(randomScaleMin, randomScaleMax) + val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) + val fixed = if (maxScale - minScale <= 1e-9) { + AxisRandomScale(minScale, minScale, minScale) + } else { + AxisRandomScale( + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale) + ) + } + particle.controler.bufferedData[randomScaleKey] = fixed + return fixed + } + + private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { + if (maxLen <= 0.0) return vec + val len = vec.length() + if (len <= 1e-9 || len <= maxLen) return vec + return vec.scale(maxLen / len) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt new file mode 100644 index 00000000..c903b551 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class ParticleLifetimeVelocityMode { + ADD, + OVERRIDE, + MULTIPLY +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt new file mode 100644 index 00000000..69a4d496 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class ParticleMotionSpace { + WORLD, + LOCAL +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt new file mode 100644 index 00000000..9486f3fa --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt @@ -0,0 +1,205 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.extend.multiply +import cn.coostack.cooparticlesapi.extend.times +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import kotlin.math.floor + +/** + * 噪声扰动(Noise / Turbulence) + * + * 作用:在每个 tick 给粒子速度叠加一股“连续的风”,让轨迹更自然、更有生命感, + * 常用于:烟雾/火焰抖动、能量流扰动、爆炸尘埃乱流、魔法粒子“活跃感”。 + * + * @property strength + * - 噪声加速度强度(每 tick 叠加到 velocity 的幅度) + * - 越大:风越“猛”,轨迹更弯、更乱、更炸;过大时会出现抽搐/抖动、偏航严重、粒子群散架。 + * - 越小:扰动更轻微,只是细腻的呼吸感/轻微摆动,整体运动仍主要由初速度决定。 + * - 经验:火焰/烟雾中等,能量微扰偏小,爆炸尘埃可偏大但必须配合 drag 或 clampSpeed。 + * + * @property frequency + * - 空间频率(噪声随“空间位置”变化的密度,决定扰动“颗粒感”) + * - 越大:噪声变化更快、更碎、更“抖”,局部细节更丰富;过大时会像高频抖动/电流乱颤。 + * - 越小:噪声变化更慢、更平滑,形成大尺度的“风带/云团”漂移;过小会显得太温柔、缺细节。 + * - 直觉:frequency 决定“风场纹理”的粗细(大=细碎,小=粗大块)。 + * + * @property speed + * - 时间滚动速度(噪声场随时间流动的快慢) + * - 越大:风场变化更快,扰动更活跃,像湍急的气流;过大时会像噪声在闪烁,产生“乱抖”的观感。 + * - 越小:风场变化更慢,像缓慢流动的空气/云层;过小会显得“静止抖动”,缺少流体感。 + * - 直觉:speed 决定“风在动得有多快”(不是风的力量,力量是 strength)。 + * + * @property affectY + * - 垂直影响比例(对 Y 方向扰动的缩放) + * - 越大:上下翻滚更明显,更“蓬/炸”,适合爆炸烟尘、魔法能量上涌;过大会显得漂浮、失重。 + * - 越小:更贴地/更平,垂直更稳定;常用于烟雾(避免上下乱跳)或贴地尘土。 + * - 常用:烟雾 0.3~0.7;能量上涌可 1.0~1.5;贴地尘土可 0.0~0.3。 + * + * @property clampSpeed + * - 限速(对最终速度做上限,防止速度叠加越来越快) + * - 越大:允许粒子跑得更快,扰动会更“飘逸/发散”;过大时可能被 strength 一直堆出很高速度。 + * - 越小:更“黏”,粒子速度被压住,群体更稳更聚;过小时会把扰动的动态感也一起压扁。 + * - 直觉:clampSpeed 是“安全阀/天花板”,用来保证效果不会失控。 + * + * @property useLifeCurve + * - 是否按生命周期调制强度(把 strength 乘上寿命曲线) + * - true:扰动会随生命周期变化(常见:出生强→死亡弱,或先弱后强),更像真实流体/能量变化。 + * - false:全程扰动强度恒定,更稳定可控,但可能显得“机械”,不够有呼吸感。 + * - 建议:大部分效果都开 true;只有想要“持续稳定噪声”时才关(例如长时间环境粒子)。 + */ +class ParticleNoiseCommand( + private var strength: Double = 0.03, + private var frequency: Double = 0.15, + private var speed: Double = 0.12, + private var affectY: Double = 1.0, + private var clampSpeed: Double = 0.8, + private var useLifeCurve: Boolean = true, +) : ParticleCommand { + constructor() : this(0.03, 0.15, 0.12, 1.0, 0.8, true) + + /** + * 设置噪声加速度强度(每 tick 叠加到 velocity 的幅度)。 + * + * - 越大:扰动更猛,轨迹更弯、更乱,需注意配合限速或阻尼 + * - 越小:扰动更轻微,仅提供细腻的呼吸感 + */ + fun strength(v: Double) = apply { strength = v } + + /** + * 设置空间频率(噪声随空间位置变化的密度)。 + * + * - 越大:扰动更细碎、更抖动 + * - 越小:扰动更平滑,形成大尺度风带 + */ + fun frequency(v: Double) = apply { frequency = v } + + /** + * 设置时间滚动速度(噪声场随时间变化的快慢)。 + * + * - 越大:风场变化更快,更活跃 + * - 越小:风场变化更慢,更沉稳 + */ + fun speed(v: Double) = apply { speed = v } + + /** + * 设置垂直方向(Y 轴)的扰动影响比例。 + * + * - 越大:上下翻滚更明显,更蓬松 + * - 越小:垂直更稳定,更贴地 + */ + fun affectY(v: Double) = apply { affectY = v } + + /** + * 设置速度上限(最终 velocity 的最大长度)。 + * + * - 用于防止噪声扰动持续叠加导致速度失控 + * - 值越大:运动更飘逸、发散 + * - 值越小:粒子群更稳、更聚 + */ + fun clampSpeed(v: Double) = apply { clampSpeed = v } + + /** + * 设置是否按生命周期曲线调制扰动强度。 + * + * - true:扰动随粒子生命周期变化,更自然、更有呼吸感 + * - false:全程恒定扰动,更稳定、可控 + */ + fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } + + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + // 0..1 生命周期进度 + val t = if (particle.lifetime > 0) particle.currentAge.toDouble() / particle.lifetime.toDouble() else 0.0 + + // 取粒子的稳定 seed(一定要稳定) + // 你可以换成 data.seed / spawnIndex / uuid hash + val seed = particle.controlUUID.hashCode() // 如果没有 id,自己做一个 + + // 时间输入:连续滚动 + val time = (particle.currentAge.toDouble()) * speed + val pos = particle.loc + // 噪声采样点:位置 * 频率 + time + val p = (Vec3(pos.x, pos.y, pos.z) * this.frequency) + .add(time, time * 0.7, time * 1.3) + + var amp = strength + if (useLifeCurve) { + // 这里你换成 data 的曲线:例如 data.strengthCurve.sample(t) + // 我给默认:前强后弱(像烟) + val lifeMul = (1.0 - t) + amp *= lifeMul + } + + val n = noiseVec3(p, seed) + val dv = Vec3(n.x, n.y * affectY, n.z).multiply(amp) + + val v0 = data.velocity + var v = v0.add(dv) + + // 限速(很重要) + val sp2 = v.lengthSqr() + val max2 = clampSpeed * clampSpeed + if (sp2 > max2) { + v = v.normalize().multiply(clampSpeed) + } + + data.velocity = v + } + + private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) + private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t + + private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { + // 纯 hash -> 0..1 + var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 + n = (n xor (n ushr 13)) * 1274126177 + n = n xor (n ushr 16) + // 转成 0..1 + return ((n and 0x7fffffff).toDouble() / 2147483647.0) + } + + private fun valueNoise3(p: Vec3, seed: Int): Double { + val x0 = floor(p.x).toInt() + val y0 = floor(p.y).toInt() + val z0 = floor(p.z).toInt() + val fx = p.x - x0 + val fy = p.y - y0 + val fz = p.z - z0 + + val u = fade(fx) + val v = fade(fy) + val w = fade(fz) + + fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) + + val n000 = h(0, 0, 0) + val n100 = h(1, 0, 0) + val n010 = h(0, 1, 0) + val n110 = h(1, 1, 0) + val n001 = h(0, 0, 1) + val n101 = h(1, 0, 1) + val n011 = h(0, 1, 1) + val n111 = h(1, 1, 1) + + val nx00 = lerp(n000, n100, u) + val nx10 = lerp(n010, n110, u) + val nx01 = lerp(n001, n101, u) + val nx11 = lerp(n011, n111, u) + + val nxy0 = lerp(nx00, nx10, v) + val nxy1 = lerp(nx01, nx11, v) + + return lerp(nxy0, nxy1, w) // 0..1 + } + + private fun noiseVec3(pos: Vec3, seed: Int): Vec3 { + // 用不同 seed 偏移得到 xyz 三通道 + val nx = valueNoise3(pos, seed + 11) * 2 - 1 + val ny = valueNoise3(pos, seed + 23) * 2 - 1 + val nz = valueNoise3(pos, seed + 37) * 2 - 1 + val v = Vec3(nx, ny, nz) + return if (v.lengthSqr() < 1e-8) Vec3.ZERO else v.normalize() + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt new file mode 100644 index 00000000..858fef4c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt @@ -0,0 +1,177 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.max + +class ParticleOrbitCommand() : ParticleCommand { + /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 目标轨道半径(世界单位) + * + * - 值越小:轨道越紧凑 + * - 值越大:粒子绕行范围越大 + */ + var radius: Double = 3.0 + + /** + * 角速度强度(每 tick 施加的切向速度增量) + * + * - 较小:缓慢绕行 + * - 较大:快速旋转(建议配合限速或阻尼) + */ + var angularSpeed: Double = 0.35 + + /** + * 径向纠正强度(用于将粒子拉回目标半径) + * + * - 值越小:允许轨道半径有较大漂移 + * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 + */ + var radialCorrect: Double = 0.25 + + /** + * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) + */ + var minDistance: Double = 0.2 + + /** + * 轨道模式 + * + * - PHYSICAL:真实物理(默认) + * - SPRING:弹簧收敛(推荐通用) + * - SNAP:强制吸附(最稳定) + */ + var mode: OrbitMode = OrbitMode.PHYSICAL + + /** + * 仅在 PHYSICAL / SPRING 下生效: + * 每 tick 最大径向修正量(防止启动时被拉飞) + */ + var maxRadialStep: Double = 0.5 + + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + radius: Double = 3.0, + angularSpeed: Double = 0.35, + radialCorrect: Double = 0.25, + minDistance: Double = 0.2, + mode: OrbitMode = OrbitMode.PHYSICAL, + ) : this() { + this.center = center + this.axis = axis + this.radius = radius + this.angularSpeed = angularSpeed + this.radialCorrect = radialCorrect + this.minDistance = minDistance + this.mode = mode + } + + /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ + fun center(v: Supplier) = apply { center = v } + + /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ + fun axis(v: Vec3) = apply { axis = v } + + /** + * 目标轨道半径(世界单位) + * + * - 值越小:轨道越紧凑 + * - 值越大:粒子绕行范围越大 + */ + fun radius(v: Double) = apply { radius = v } + + /** + * 角速度强度(每 tick 施加的切向速度增量) + * + * - 较小:缓慢绕行 + * - 较大:快速旋转(建议配合限速或阻尼) + */ + fun angularSpeed(v: Double) = apply { angularSpeed = v } + + /** + * 径向纠正强度(用于将粒子拉回目标半径) + * + * - 值越小:允许轨道半径有较大漂移 + * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 + */ + fun radialCorrect(v: Double) = apply { radialCorrect = v } + + /** + * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) + */ + fun minDistance(v: Double) = apply { minDistance = v } + + /** + * 轨道模式 + * + * - PHYSICAL:真实物理(默认) + * - SPRING:弹簧收敛(推荐通用) + * - SNAP:强制吸附(最稳定) + */ + fun mode(v: OrbitMode) = apply { mode = v } + + /** + * 仅在 PHYSICAL / SPRING 下生效: + * 每 tick 最大径向修正量(防止启动时被拉飞) + */ + fun maxRadialStep(v: Double) = apply { maxRadialStep = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val ax = axis.normalize() + val centerPos = center.get() + + val r0 = pos.subtract(centerPos) + val axialComp = ax.scale(r0.dot(ax)) + var radial = r0.subtract(axialComp) + + var dist = radial.length() + dist = max(dist, minDistance) + + radial = radial.scale(1.0 / dist) + + // 切向方向 + var tangential = ax.cross(radial) + val tLen = tangential.length() + if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) + + val dvTan = tangential.scale(angularSpeed) + val err = dist - radius + + when (mode) { + OrbitMode.PHYSICAL -> { + val raw = -err * radialCorrect + val step = raw.coerceIn(-maxRadialStep, maxRadialStep) + val dvRad = radial.scale(step) + data.velocity = data.velocity.add(dvTan).add(dvRad) + } + + OrbitMode.SPRING -> { + // 二阶感:误差越大,拉力越强,但更温和 + val spring = -err * radialCorrect + val step = spring.coerceIn(-maxRadialStep, maxRadialStep) + val dvRad = radial.scale(step) + data.velocity = data.velocity.add(dvTan).add(dvRad) + } + + OrbitMode.SNAP -> { + // 直接把粒子“吸附”到目标半径 + val targetPos = centerPos + .add(axialComp) + .add(radial.scale(radius)) + + val snapVec = targetPos.subtract(pos) + data.velocity = data.velocity.add(dvTan).add(snapVec.scale(radialCorrect)) + } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt new file mode 100644 index 00000000..29e483e5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt @@ -0,0 +1,98 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier + +/** + * 旋转力:让粒子绕某个轴产生切向速度(像被“搅拌”)。 + * + * 典型效果: + * - 火焰上升的旋涡感 + * - 魔法阵环绕 + * - 爆炸烟雾“打旋” + */ +class ParticleRotationForceCommand() : ParticleCommand { + + /** 旋转中心(世界坐标)。 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** + * 旋转轴(会被自动 normalize)。 + * + * - (0,1,0) 绕 Y 轴旋转(最常用) + * - (1,0,0) 绕 X + * - (0,0,1) 绕 Z + */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 旋转强度(每 tick 切向速度增量)。 + * + * - 0.05 ~ 0.3:轻微旋 + * - 0.3 ~ 2.0:明显旋 + * - >2.0:非常暴力(建议配合阻尼/限速) + */ + var strength: Double = 0.35 + + /** + * 距离衰减范围(>0)。 + * + * - 越大:远处仍然能被带动旋转 + * - 越小:只在中心附近打旋 + */ + var range: Double = 8.0 + + /** 衰减幂次(>=1)。 */ + var falloffPower: Double = 2.0 + + /** + * @param center 旋转中心 + * @param axis 旋转轴(自动 normalize) + * @param strength 旋转强度(每 tick 切向速度增量) + * @param range 衰减范围尺度 + * @param falloffPower 衰减幂次(>=1) + */ + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + strength: Double = 0.35, + range: Double = 8.0, + falloffPower: Double = 2.0, + ) : this() { + this.center = center + this.axis = axis + this.strength = strength + this.range = range + this.falloffPower = falloffPower + } + + fun center(v: Supplier) = apply { + center = v + } + fun axis(v: Vec3) = apply { axis = v } + fun strength(v: Double) = apply { strength = v } + fun range(v: Double) = apply { range = v } + fun falloffPower(v: Double) = apply { falloffPower = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val r = pos.subtract(center.get()) + val dist = r.length() + if (dist < 1e-9) return + + val ax = axis.normalize() + // tangential direction: axis x r + var t = ax.cross(r) + val tLen = t.length() + if (tLen < 1e-9) return + t = t.scale(1.0 / tLen) + + val falloff = GraphMathHelper.inversePowerFalloff(dist, range, falloffPower) + val dv = t.scale(strength * falloff) + + data.velocity = data.velocity.add(dv) + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt new file mode 100644 index 00000000..69a591f2 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt @@ -0,0 +1,226 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.extend.plus +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.abs +import kotlin.math.sqrt + +/** + * 环面回流场。 + * + * 在一圈局部区域里制造“翻卷”轨迹,适合蘑菇云帽檐、烟团边缘这类效果。 + * 它会优先“扭转当前速度方向”,而不是把粒子硬拽成一个环, + * 所以在有阻尼/减速时,卷动也会跟着自然变慢。 + * 它改的是粒子速度,不是 billboard 朝向。 + */ +class ParticleToroidalCirculationCommand : ParticleCommand { + /** 环流中心。做蘑菇云时一般填帽子中心。 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** 主轴,默认 Y 轴。 */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** 主半径,也就是翻卷带离中心的距离。 */ + var ringRadius: Double = 3.0 + + /** 径向厚度。 */ + var radialThickness: Double = 1.2 + + /** 轴向厚度。 */ + var axialThickness: Double = 0.8 + + /** 翻卷力度,负数表示反向。 */ + var circulationStrength: Double = 0.35 + + /** 向外撑开的附加力度。 */ + var outwardStrength: Double = 0.0 + + /** 向上抬的附加力度。 */ + var upwardStrength: Double = 0.0 + + /** 往翻卷带回带/导向的力度。主要用于带外把速度重新导回卷动区。 */ + var followStrength: Double = 0.12 + + /** 单 tick 最大修正量。<=0 不限制。 */ + var maxStep: Double = 0.6 + + /** 是否按生命周期减弱。 */ + var useLifeCurve: Boolean = false + + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + ringRadius: Double = 3.0, + radialThickness: Double = 1.2, + axialThickness: Double = 0.8, + circulationStrength: Double = 0.35, + outwardStrength: Double = 0.0, + upwardStrength: Double = 0.0, + followStrength: Double = 0.12, + maxStep: Double = 0.6, + useLifeCurve: Boolean = false, + ) { + this.center = center + this.axis = axis + this.ringRadius = ringRadius + this.radialThickness = radialThickness + this.axialThickness = axialThickness + this.circulationStrength = circulationStrength + this.outwardStrength = outwardStrength + this.upwardStrength = upwardStrength + this.followStrength = followStrength + this.maxStep = maxStep + this.useLifeCurve = useLifeCurve + } + + fun center(v: Supplier) = apply { center = v } + + fun center(v: Vec3) = apply { center = Supplier { v } } + + fun center(x: Double, y: Double, z: Double) = apply { center = Supplier { Vec3(x, y, z) } } + + fun axis(v: Vec3) = apply { axis = v } + + fun axis(x: Double, y: Double, z: Double) = apply { axis = Vec3(x, y, z) } + + fun ringRadius(v: Double) = apply { ringRadius = v } + + fun radialThickness(v: Double) = apply { radialThickness = v } + + fun axialThickness(v: Double) = apply { axialThickness = v } + + /** 同时设置两个厚度,方便网页只留一个输入框。 */ + fun thickness(v: Double) = apply { + radialThickness = v + axialThickness = v + } + + fun circulationStrength(v: Double) = apply { circulationStrength = v } + + + fun outwardStrength(v: Double) = apply { outwardStrength = v } + + fun upwardStrength(v: Double) = apply { upwardStrength = v } + + fun followStrength(v: Double) = apply { followStrength = v } + + fun maxStep(v: Double) = apply { maxStep = v } + + fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val centerPos = center.get() + val ax = safeNormalize(axis) + val pos = particle.loc + val rel = pos.subtract(centerPos) + + val axialDistance = rel.dot(ax) + val planar = rel.subtract(ax.scale(axialDistance)) + val planarLen = planar.length() + + val radialDir = if (planarLen < 1e-9) { + anyPerp(ax) + } else { + planar.scale(1.0 / planarLen) + } + + val qr = planarLen - ringRadius + + val radialSize = radialThickness.coerceAtLeast(1e-6) + val axialSize = axialThickness.coerceAtLeast(1e-6) + + val localRadial = qr / radialSize + val localAxial = axialDistance / axialSize + val normalizedDistance = sqrt(localRadial * localRadial + localAxial * localAxial) + + val insideBand = normalizedDistance < 1.0 + val bandWeight = if (insideBand) smooth01(1.0 - normalizedDistance) else 0.0 + val outerDistance = (normalizedDistance - 1.0).coerceAtLeast(0.0) + val captureWeight = if (outerDistance > 0.0) { + 1.0 / (1.0 + outerDistance * outerDistance * 4.0) + } else { + 0.0 + } + val lifeMul = if (useLifeCurve && particle.lifetime > 0) { + (1.0 - particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) + } else { + 1.0 + } + if (lifeMul <= 1e-9) { + return + } + + val currentVelocity = if (data.velocity.lengthSqr() > 1e-12) data.velocity else particle.velocity + val currentSpeed = currentVelocity.length() + if (currentSpeed <= 1e-9) { + return + } + val currentDir = currentVelocity.scale(1.0 / currentSpeed) + + val circulationVector = radialDir.scale(-localAxial) + .add(ax.scale(localRadial)) + val toBandCenter = radialDir.scale(-localRadial).add(ax.scale(-localAxial)) + + var desiredFlow = Vec3.ZERO + + val circulationWeight = bandWeight * lifeMul + if (circulationStrength != 0.0 && circulationWeight > 1e-9 && circulationVector.lengthSqr() > 1e-12) { + desiredFlow = desiredFlow.add(circulationVector.scale(circulationStrength * circulationWeight)) + } + if (insideBand && outwardStrength != 0.0 && circulationWeight > 1e-9) { + desiredFlow = desiredFlow.add(radialDir.scale(outwardStrength * circulationWeight)) + } + if (insideBand && upwardStrength != 0.0 && circulationWeight > 1e-9) { + desiredFlow = desiredFlow.add(ax.scale(upwardStrength * circulationWeight)) + } + + val captureMul = captureWeight * lifeMul + if (followStrength != 0.0 && captureMul > 1e-9 && toBandCenter.lengthSqr() > 1e-12) { + desiredFlow = desiredFlow.add(normalizeOrZero(toBandCenter).scale(followStrength * captureMul)) + } + + val desiredFlowLen = desiredFlow.length() + if (desiredFlowLen <= 1e-9) { + return + } + val desiredDir = desiredFlow.scale(1.0 / desiredFlowLen) + val turnedDir = normalizeOrZero(currentDir.add(desiredDir.scale(desiredFlowLen))) + if (turnedDir.lengthSqr() <= 1e-12) { + return + } + + var dv = turnedDir.scale(currentSpeed).subtract(currentVelocity) + + val dvLen = dv.length() + if (maxStep > 0.0 && dvLen > maxStep) { + dv = dv.scale(maxStep / dvLen) + } + + data.velocity += dv + } + + private fun safeNormalize(v: Vec3): Vec3 { + val len = v.length() + return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) + } + + private fun normalizeOrZero(v: Vec3): Vec3 { + val len = v.length() + return if (len < 1e-9) Vec3.ZERO else v.scale(1.0 / len) + } + + private fun anyPerp(axis: Vec3): Vec3 { + val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) + val perp = axis.cross(ref) + val len = perp.length() + return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) + } + + private fun smooth01(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + return clamped * clamped * (3.0 - 2.0 * clamped) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt new file mode 100644 index 00000000..4c86af8d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt @@ -0,0 +1,161 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.max + +/** + * 漩涡:让粒子围绕轴旋转,并可向轴线“吸入”,以及沿轴“上升/下沉”。 + * + * 常用组合: + * - swirlStrength > 0 : 打旋 + * - radialPull > 0 : 往轴线收拢(像龙卷风) + * - axialLift > 0 : 沿轴向上(像火焰/风柱) + */ +class ParticleVortexCommand() : ParticleCommand { + + /** 漩涡中心点(轴线穿过此点)。 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** 轴方向(自动 normalize)。 */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 切向旋转强度(每 tick 切向速度增量) + * + * - 0.1 ~ 0.8:柔和漩涡 + * - 0.8 ~ 3.0:明显龙卷感 + */ + var swirlStrength: Double = 0.8 + + /** + * 向轴线吸入的强度(每 tick 径向速度增量) + * + * - 0:不收拢(只打旋) + * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” + */ + var radialPull: Double = 0.35 + + /** + * 沿轴方向的升力(每 tick 轴向速度增量) + * + * - >0:沿轴正方向上升 + * - <0:沿轴反方向下沉 + */ + var axialLift: Double = 0.0 + + /** 衰减范围尺度(>0)。 */ + var range: Double = 10.0 + + /** 衰减幂次(>=1)。 */ + var falloffPower: Double = 2.0 + + /** 避免 r=0 的最小距离钳制 */ + var minDistance: Double = 0.2 + + /** + * @param center 漩涡中心(轴线穿过点) + * @param axis 轴方向(自动 normalize) + * @param swirlStrength 旋转强度(切向) + * @param radialPull 径向吸入强度 + * @param axialLift 轴向升力 + * @param range 衰减范围尺度 + * @param falloffPower 衰减幂次 + * @param minDistance 最小距离钳制,避免抖动/爆炸 + */ + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + swirlStrength: Double = 0.8, + radialPull: Double = 0.35, + axialLift: Double = 0.0, + range: Double = 10.0, + falloffPower: Double = 2.0, + minDistance: Double = 0.2, + ) : this() { + this.center = center + this.axis = axis + this.swirlStrength = swirlStrength + this.radialPull = radialPull + this.axialLift = axialLift + this.range = range + this.falloffPower = falloffPower + this.minDistance = minDistance + } + + /** 漩涡中心点(轴线穿过此点)。 */ + fun center(v: Supplier) = apply { center = v } + + /** 轴方向(自动 normalize)。 */ + fun axis(v: Vec3) = apply { axis = v } + + /** + * 切向旋转强度(每 tick 切向速度增量) + * + * - 0.1 ~ 0.8:柔和漩涡 + * - 0.8 ~ 3.0:明显龙卷感 + */ + fun swirlStrength(v: Double) = apply { swirlStrength = v } + + /** + * 向轴线吸入的强度(每 tick 径向速度增量) + * + * - 0:不收拢(只打旋) + * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” + */ + fun radialPull(v: Double) = apply { radialPull = v } + + /** + * 沿轴方向的升力(每 tick 轴向速度增量) + * + * - >0:沿轴正方向上升 + * - <0:沿轴反方向下沉 + */ + fun axialLift(v: Double) = apply { axialLift = v } + + /** 衰减范围尺度(>0)。 */ + fun range(v: Double) = apply { range = v } + + /** 衰减幂次(>=1)。 */ + fun falloffPower(v: Double) = apply { falloffPower = v } + + /** 避免 r=0 的最小距离钳制 */ + fun minDistance(v: Double) = apply { minDistance = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val ax = axis.normalize() + + // vector from center + val r = pos.subtract(center.get()) + + // remove axial component -> radial vector to axis line + val axialComp = ax.scale(r.dot(ax)) + val radial = r.subtract(axialComp) + + var dist = radial.length() + if (dist < 1e-9) dist = 0.0 + + val d = max(dist, minDistance) + val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) + + // tangential direction + var tangential = ax.cross(radial) + val tLen = tangential.length() + if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) + + // radial inward direction + var inward = Vec3.ZERO + val rLen = radial.length() + if (rLen > 1e-9) inward = radial.scale(-1.0 / rLen) + + val dv = tangential.scale(swirlStrength * falloff) + .add(inward.scale(radialPull * falloff)) + .add(ax.scale(axialLift * falloff)) + + data.velocity = data.velocity.add(dv) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt new file mode 100644 index 00000000..618855d8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt @@ -0,0 +1,10 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +data class BezierFloatKeyframe( + val time: Double, + val value: Double, + val outX: Double = 0.0, + val outY: Double = 0.0, + val inX: Double = 0.0, + val inY: Double = 0.0 +) diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt new file mode 100644 index 00000000..b8838ff9 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt @@ -0,0 +1,250 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +import kotlin.math.abs +import kotlin.math.sqrt + +class BezierKeyframeFloatCurve( + frames: List = listOf(BezierFloatKeyframe(0.0, 0.0)) +) : FloatCurve { + private val keyframes: MutableList = + frames.map { it.withClampedTime() } + .sortedBy { it.time } + .toMutableList() + + init { + if (keyframes.isEmpty()) { + keyframes.add(BezierFloatKeyframe(0.0, 0.0)) + } + } + + fun setFrames(frames: List) = apply { + keyframes.clear() + keyframes.addAll( + frames.map { it.withClampedTime() } + .sortedBy { it.time } + ) + if (keyframes.isEmpty()) { + keyframes.add(BezierFloatKeyframe(0.0, 0.0)) + } + } + + fun addFrame( + time: Double, + value: Double, + outX: Double = 0.0, + outY: Double = 0.0, + inX: Double = 0.0, + inY: Double = 0.0 + ) = apply { + keyframes.add( + BezierFloatKeyframe( + time = time.coerceIn(0.0, 1.0), + value = value, + outX = outX, + outY = outY, + inX = inX, + inY = inY + ) + ) + keyframes.sortBy { it.time } + } + + /** + * 返回当前锚点和控制柄的不可变快照。 + * + * 示例:`CParticleCurve.fromFloatCurve(curve)` 使用此快照保留 GPU 控制柄。 + * 禁止:修改返回列表不会改变此曲线;需要修改时应调用 [setFrames]。 + * + * @return 按时间升序排列的当前关键帧 + */ + fun frames(): List = keyframes.toList() + + override fun sample(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + if (keyframes.size == 1) { + return keyframes[0].value + } + if (clamped <= keyframes.first().time) { + return keyframes.first().value + } + if (clamped >= keyframes.last().time) { + return keyframes.last().value + } + + for (i in 1 until keyframes.size) { + val prev = keyframes[i - 1] + val next = keyframes[i] + if (clamped <= next.time) { + return sampleSegment(prev, next, clamped) + } + } + return keyframes.last().value + } + + private fun sampleSegment(prev: BezierFloatKeyframe, next: BezierFloatKeyframe, time: Double): Double { + val span = next.time - prev.time + if (span <= EPSILON) { + return next.value + } + + val p0x = prev.time + val p1x = prev.time + prev.outX / 100.0 + val p2x = next.time + next.inX / 100.0 + val p3x = next.time + + val p0y = prev.value + val p1y = prev.value + prev.outY + val p2y = next.value + next.inY + val p3y = next.value + + val linearGuess = ((time - prev.time) / span).coerceIn(0.0, 1.0) + val u = solveParameterForX(time, p0x, p1x, p2x, p3x, linearGuess) + return cubicBezier(u, p0y, p1y, p2y, p3y) + } + + private fun solveParameterForX( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double, + guess: Double + ): Double { + if (p0 <= p1 && p1 <= p2 && p2 <= p3) { + return solveMonotonicParameter(targetX, p0, p1, p2, p3) + } + + val roots = mutableListOf() + val splitPoints = derivativeRoots(p0, p1, p2, p3) + .filter { it > EPSILON && it < 1.0 - EPSILON } + .plus(0.0) + .plus(1.0) + .sorted() + + for (i in 1 until splitPoints.size) { + val lo = splitPoints[i - 1] + val hi = splitPoints[i] + val flo = cubicBezier(lo, p0, p1, p2, p3) - targetX + val fhi = cubicBezier(hi, p0, p1, p2, p3) - targetX + when { + abs(flo) <= EPSILON -> roots.add(lo) + abs(fhi) <= EPSILON -> roots.add(hi) + flo < 0.0 && fhi > 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) + flo > 0.0 && fhi < 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) + } + } + + return roots.distinctBy { (it / EPSILON).toLong() } + .minByOrNull { abs(it - guess) } + ?: closestSampledParameter(targetX, p0, p1, p2, p3, guess) + } + + private fun solveMonotonicParameter( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double + ): Double { + var lo = 0.0 + var hi = 1.0 + var mid = 0.5 + repeat(32) { + mid = (lo + hi) * 0.5 + if (cubicBezier(mid, p0, p1, p2, p3) < targetX) { + lo = mid + } else { + hi = mid + } + } + return mid + } + + private fun solveBracketedParameter( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double, + startLo: Double, + startHi: Double + ): Double { + var lo = startLo + var hi = startHi + var mid = (lo + hi) * 0.5 + val increasing = cubicBezier(hi, p0, p1, p2, p3) >= cubicBezier(lo, p0, p1, p2, p3) + repeat(32) { + mid = (lo + hi) * 0.5 + val x = cubicBezier(mid, p0, p1, p2, p3) + if ((x < targetX) == increasing) { + lo = mid + } else { + hi = mid + } + } + return mid + } + + private fun derivativeRoots(p0: Double, p1: Double, p2: Double, p3: Double): List { + val a = -p0 + 3.0 * p1 - 3.0 * p2 + p3 + val b = 3.0 * p0 - 6.0 * p1 + 3.0 * p2 + val c = -3.0 * p0 + 3.0 * p1 + + val qa = 3.0 * a + val qb = 2.0 * b + val qc = c + if (abs(qa) <= EPSILON) { + return if (abs(qb) <= EPSILON) emptyList() else listOf(-qc / qb) + } + + val discriminant = qb * qb - 4.0 * qa * qc + if (discriminant < 0.0) { + return emptyList() + } + + val root = sqrt(discriminant) + return listOf( + (-qb - root) / (2.0 * qa), + (-qb + root) / (2.0 * qa) + ) + } + + private fun closestSampledParameter( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double, + guess: Double + ): Double { + var best = guess + var bestError = abs(cubicBezier(guess, p0, p1, p2, p3) - targetX) + for (i in 0..64) { + val u = i / 64.0 + val error = abs(cubicBezier(u, p0, p1, p2, p3) - targetX) + if (error < bestError) { + best = u + bestError = error + } + } + return best + } + + private fun cubicBezier(t: Double, p0: Double, p1: Double, p2: Double, p3: Double): Double { + val u = 1.0 - t + val u2 = u * u + val t2 = t * t + return u2 * u * p0 + + 3.0 * u2 * t * p1 + + 3.0 * u * t2 * p2 + + t2 * t * p3 + } + + private fun BezierFloatKeyframe.withClampedTime(): BezierFloatKeyframe { + return copy(time = time.coerceIn(0.0, 1.0)) + } + + private companion object { + const val EPSILON = 1.0E-9 + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt new file mode 100644 index 00000000..2717be1f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +/** + * 常量曲线:全生命周期输出同一个值。 + * + * 典型用途: + * - 固定重力倍率 + * - 固定速度修正 + * - 作为其它更复杂曲线的默认占位 + */ +class ConstantFloatCurve( + /** + * 常量值。 + */ + var value: Double = 0.0 +) : FloatCurve { + override fun sample(t: Double): Double = value +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt new file mode 100644 index 00000000..2998ffa8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt @@ -0,0 +1,22 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +/** + * 生命周期曲线接口(标量)。 + * + * 约定: + * - 输入 `t` 是生命周期进度,通常期望在 `[0,1]` 范围内 + * - 实现类应尽量在内部做 `t` 的钳制,避免调用方传入异常值时不稳定 + * + * 用途: + * - 给粒子命令(如 Force/Velocity/Inherit)提供“随时间变化”的系数 + * - 统一不同曲线实现(常量、线性、关键帧)的采样入口 + */ +fun interface FloatCurve { + /** + * 采样曲线值。 + * + * @param t 生命周期进度(通常是 `currentAge / lifetime`) + * @return 曲线在该进度下的值 + */ + fun sample(t: Double): Double +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt new file mode 100644 index 00000000..d8953cf5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt @@ -0,0 +1,12 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +/** + * 标量关键帧。 + * + * @property time 关键帧时间点,建议使用 `[0,1]` 的生命周期归一化时间 + * @property value 关键帧数值 + */ +data class FloatKeyframe( + val time: Double, + val value: Double +) diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt new file mode 100644 index 00000000..56253661 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt @@ -0,0 +1,78 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +/** + * 关键帧曲线(线性插值)。 + * + * 规则: + * - 帧会按 `time` 升序保存 + * - `t` 落在相邻关键帧之间时,使用线性插值 + * - `t` 小于首帧或大于尾帧时,返回边界帧值 + */ +class KeyframeFloatCurve( + frames: List = listOf(FloatKeyframe(0.0, 0.0)) +) : FloatCurve { + private val keyframes: MutableList = + frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } + .sortedBy { it.time } + .toMutableList() + + init { + if (keyframes.isEmpty()) { + keyframes.add(FloatKeyframe(0.0, 0.0)) + } + } + + /** + * 批量替换关键帧列表。 + * + * - 时间会被钳制到 `[0,1]` + * - 内部会自动按时间升序排序 + */ + fun setFrames(frames: List) = apply { + keyframes.clear() + keyframes.addAll( + frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } + .sortedBy { it.time } + ) + if (keyframes.isEmpty()) { + keyframes.add(FloatKeyframe(0.0, 0.0)) + } + } + + /** + * 追加一个关键帧。 + * + * @param time 帧时间(会钳制到 `[0,1]`) + * @param value 帧值 + */ + fun addFrame(time: Double, value: Double) = apply { + keyframes.add(FloatKeyframe(time.coerceIn(0.0, 1.0), value)) + keyframes.sortBy { it.time } + } + + override fun sample(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + if (keyframes.size == 1) { + return keyframes[0].value + } + if (clamped <= keyframes.first().time) { + return keyframes.first().value + } + if (clamped >= keyframes.last().time) { + return keyframes.last().value + } + + for (i in 1 until keyframes.size) { + val prev = keyframes[i - 1] + val next = keyframes[i] + if (clamped <= next.time) { + val seg = (next.time - prev.time).coerceAtLeast(1e-9) + val alpha = (clamped - prev.time) / seg + return GraphMathHelper.lerp(alpha, prev.value, next.value) + } + } + return keyframes.last().value + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt new file mode 100644 index 00000000..e11c75e5 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt @@ -0,0 +1,27 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +/** + * 线性曲线:在 `[0,1]` 区间内从 `from` 匀速过渡到 `to`。 + * + * 典型用途: + * - 生命周期内“线性增强/衰减” + * - 先快速验证一个效果趋势,再换成关键帧曲线精调 + */ +class LinearFloatCurve( + /** + * `t=0` 时的值。 + */ + var from: Double = 0.0, + + /** + * `t=1` 时的值。 + */ + var to: Double = 1.0 +) : FloatCurve { + override fun sample(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + return GraphMathHelper.lerp(clamped, from, to) + } +} 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..d9319bd3 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt @@ -0,0 +1,77 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import com.ezylang.evalex.Expression +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 + +class BallWindDirection( + override var direction: Vec3, + var radius: Double, + var offset: RelativeLocation, +) : WindDirection { + override var relative: Boolean = false + override var windSpeedExpress: String = "1" + override fun loadEmitters(emitters: ParticleEmitters): WindDirection { + this.emitters = emitters + return this + } + + override fun hasLoadedEmitters(): Boolean { + return emitters != null + } + + private var emitters: ParticleEmitters? = null + + companion object { + @JvmStatic + val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, data -> + data as BallWindDirection + buf.writeVec3(data.direction) + buf.writeBoolean(data.relative) + buf.writeUtf(data.windSpeedExpress) + buf.writeVec3(data.offset.toVector()) + buf.writeDouble(data.radius) + }, { + val direction = it.readVec3() + val relative = it.readBoolean() + val express = it.readUtf() + val offset = RelativeLocation.of(it.readVec3()) + val radius = it.readDouble() + BallWindDirection(direction, radius, offset).apply { + this.relative = relative + this.windSpeedExpress = express + } + } + ) + const val ID = "ball" + } + + override fun getID(): String { + return ID + } + + override fun getWind(particlePos: Vec3): Vec3 { + if (relative) { + val pos = emitters?.pos ?: return direction + val dir = particlePos.subtract(pos) + val express = Expression(windSpeedExpress) + .with("l", dir.length()) + .evaluate().numberValue.toDouble() + dir.normalize().scale(express) + return dir + } + return direction + } + + override fun inRange(pos: Vec3): Boolean { + return emitters!!.pos.add(offset.toVector()).subtract(pos).length() <= radius + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } +} \ No newline at end of file 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..cb6f21dc --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt @@ -0,0 +1,92 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.barrages.HitBox +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import com.ezylang.evalex.Expression +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 +import kotlin.math.exp + +class BoxWindDirection( + override var direction: Vec3, + var box: HitBox, + var offset: RelativeLocation, +) : WindDirection { + override var relative: Boolean = false + override var windSpeedExpress: String = "1" + override fun loadEmitters(emitters: ParticleEmitters): WindDirection { + this.emitters = emitters + return this + } + + private var emitters: ParticleEmitters? = null + + companion object { + @JvmStatic + val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, data -> + data as BoxWindDirection + buf.writeVec3(data.direction) + buf.writeBoolean(data.relative) + buf.writeUtf(data.windSpeedExpress) + buf.writeVec3(data.offset.toVector()) + buf.writeDouble(data.box.x1) + buf.writeDouble(data.box.x2) + buf.writeDouble(data.box.y1) + buf.writeDouble(data.box.y2) + buf.writeDouble(data.box.z1) + buf.writeDouble(data.box.z2) + }, { + val direction = it.readVec3() + val relative = it.readBoolean() + val express = it.readUtf() + val offset = RelativeLocation.of(it.readVec3()) + val box = HitBox( + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + ) + BoxWindDirection(direction, box, offset).apply { + this.relative = relative + this.windSpeedExpress = express + } + } + ) + const val ID = "box" + } + + override fun getID(): String { + return ID + } + + override fun hasLoadedEmitters(): Boolean { + return emitters != null + } + + override fun getWind(particlePos: Vec3): Vec3 { + if (relative) { + val pos = emitters?.pos ?: return direction + val dir = particlePos.subtract(pos) + val express = Expression(windSpeedExpress) + .with("l", dir.length()) + .evaluate().numberValue.toDouble() + dir.normalize().scale(express) + return dir + } + return direction + } + + override fun inRange(pos: Vec3): Boolean { + val ofBox = box.ofBox(emitters!!.pos.add(offset.toVector())) + return ofBox.contains(pos) + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } +} \ No newline at end of file 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..13b2f8b6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt @@ -0,0 +1,68 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import com.ezylang.evalex.Expression +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 +import kotlin.math.exp + +class GlobalWindDirection( + override var direction: Vec3 +) : WindDirection { + override var relative: Boolean = false + override var windSpeedExpress: String = "1" + override fun loadEmitters(emitters: ParticleEmitters): WindDirection { + this.emitters = emitters + return this + } + + private var emitters: ParticleEmitters? = null + + companion object { + @JvmStatic + val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, data -> + buf.writeVec3(data.direction) + buf.writeBoolean(data.relative) + buf.writeUtf(data.windSpeedExpress) + }, { + val direction = it.readVec3() + val relative = it.readBoolean() + val express = it.readUtf() + GlobalWindDirection(direction).apply { + this.relative = relative + this.windSpeedExpress = express + } + } + ) + const val ID = "global" + } + + override fun getID(): String { + return ID + } + override fun hasLoadedEmitters(): Boolean { + return emitters != null + } + override fun getWind(particlePos: Vec3): Vec3 { + if (relative) { + val pos = emitters?.pos ?: return direction + val dir = particlePos.subtract(pos) + val express = Expression(windSpeedExpress) + .with("l", dir.length()) + .evaluate().numberValue.toDouble() + dir.normalize().scale(express) + return dir + } + return direction + } + + override fun inRange(pos: Vec3): Boolean { + return true + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } +} \ No newline at end of file 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..f662470b --- /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 cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 + +interface WindDirection { + /** + * 风向 + * 如果relative 为true 则此选项作废 + * direction.length() 代表风速大小 + * 如果设置为0向量 则代表没有风 + * relative为true 时代表粒子相对发射器位置为风向 + */ + var direction: Vec3 + var relative: Boolean + + /** + * relative 为false时此功能不启用 + * 若relative为true + * 则此参数代表风力大小 + * 表达式采用EvalEx3.5.0 + * 提供的变量 l (代表粒子与发射器的实际距离) + * 返回值是风速大小(direction.length()) + */ + var windSpeedExpress: String + + fun loadEmitters(emitters: ParticleEmitters): WindDirection + + fun hasLoadedEmitters(): Boolean + + fun getID(): String + + /** + * 获得当前位置的风速向量 + */ + fun getWind(particlePos: Vec3): Vec3 + + /** + * 粒子位置是否在可操作范围内 + */ + fun inRange(pos: Vec3): Boolean + + fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec +} \ No newline at end of file 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..ae33dc38 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt @@ -0,0 +1,57 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 + +object WindDirections { + private val packets = HashMap>() + + val GLOBAL = register("global", GlobalWindDirection.CODEC) + + val BOX = register("box", BoxWindDirection.CODEC) + + val BALL = register("ball", BallWindDirection.CODEC) + + /** + * 物理处理风力 + * @param v 先前移动方向 + * @param pos 在风场中的位置 (在粒子发射器中就是粒子位置) 如果超出范围则会返回0向量 + */ + fun handleWindForce( + wind: WindDirection, + pos: Vec3, + airDensity: Double, + dragCoefficient: Double, + crossSectionalArea: Double, + v: Vec3 = Vec3.ZERO, + ): Vec3 { + if (!wind.inRange(pos)) { + return Vec3.ZERO + } + val windVec = wind.getWind(pos) + return if (windVec.lengthSqr() > 0) { + val relativeWind = windVec.subtract(v) + val windMagnitude = 0.5 * airDensity * dragCoefficient * + crossSectionalArea * relativeWind.lengthSqr() * 0.05 + relativeWind.normalize().scale(windMagnitude) + } else { + Vec3.ZERO + } + } + + fun getCodecFromID(id: String): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return packets[id]!! + } + + fun register( + id: String, + codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + ): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + packets[id] = codec + return codec + } + + fun init() {} +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt new file mode 100644 index 00000000..0ed4fbff --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt @@ -0,0 +1,25 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.BlockHitResult + +/** + * 粒子碰撞方块事件 + * 用于复用物理粒子 + */ +class ParticleCollideEvent( + override var particle: ControlableParticle, + override var particleData: ControlableParticleData, + var res: BlockHitResult +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleColliderEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt new file mode 100644 index 00000000..9d7a3cad --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.particles.ControlableParticle + +interface ParticleEvent { + /** + * 触发事件的粒子 + */ + var particle: ControlableParticle + var particleData: ControlableParticleData + + /** + * 取消接下来的粒子事件队列 + */ + var canceled: Boolean + + /** + * 用于方便存储 + * 获取事件ID + */ + fun getEventID(): String +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt new file mode 100644 index 00000000..e6a1b67b --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt @@ -0,0 +1,26 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +/** + * 自动注册 + * 事件处理器是单例模式 + */ +interface ParticleEventHandler : Comparable { + + fun handle(event: ParticleEvent) + + /** + * 详细见 + * 目标事件的EventID + * @see ParticleEvent.getEventID + */ + fun getTargetEventID(): String + + fun getHandlerID(): String + + fun getPriority(): Int + + override fun compareTo(other: ParticleEventHandler): Int { + return getPriority() - other.getPriority() + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt new file mode 100644 index 00000000..7a2a4446 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt @@ -0,0 +1,56 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo +import cn.coostack.cooparticlesapi.utils.ReflectUtil +import java.lang.reflect.Modifier + +object ParticleEventHandlerManager { + private val registerHandlers = HashMap() + + fun getHandlerById(id: String): ParticleEventHandler? { + return registerHandlers[id] + } + + fun register(event: ParticleEventHandler) { + registerHandlers[event.getHandlerID()] = event + } + + fun hasRegister(id: String): Boolean = getHandlerById(id) != null + + private var handled = false + fun registerScanner() { + if (handled) { + return + } + val before = registerHandlers.size + CooParticlesConstants.logger.info("正在自动注册 ParticleEventHandler") + var count = 0 + val start = System.currentTimeMillis() + CooAPIScanner.getWithAnnotation( + CooAutoRegister::class.java + ).forEach { + count++ + findListenerHandlers(it) + } + val end = System.currentTimeMillis() + CooParticlesConstants.logger.info("EmittersEvents 注册完成 耗时 ${end - start} ms 扫描了 $count 个类 实际注册 :${registerHandlers.size - before}") + } + + private fun findListenerHandlers(target: SimpleClassInfo) { + val clazz = target.toClass() + if (!ParticleEvent::class.java.isAssignableFrom(clazz)) { + return + } + // 获取instance + val instance = + clazz.declaredFields.find { it.name == "INSTANCE" && Modifier.isStatic(it.modifiers) }?.get(null) + ?: clazz.getDeclaredConstructor() + .apply { isAccessible = true } + .newInstance() + register(instance as ParticleEventHandler) + } + +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt new file mode 100644 index 00000000..a1a7f892 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt @@ -0,0 +1,23 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.entity.Entity + +/** + * 粒子接触实体事件 + */ + +class ParticleHitEntityEvent( + override var particle: ControlableParticle, override var particleData: ControlableParticleData, var hit: Entity, +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleHitEntityEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt new file mode 100644 index 00000000..3175ae3c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.Vec3 + +/** + * 粒子接触地面事件 + */ +class ParticleOnGroundEvent( + override var particle: ControlableParticle, + override var particleData: ControlableParticleData, + /** + * 击中的方块 + */ + var hit: BlockPos, + /** + * 和方块的交点 + */ + var intersection: Vec3, + var res: BlockHitResult +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleOnGroundEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt new file mode 100644 index 00000000..991cc368 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.core.BlockPos + +/** + * 粒子接触实体事件 + */ +class ParticleOnLiquidEvent( + override var particle: ControlableParticle, + override var particleData: ControlableParticleData, + var hit: BlockPos +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleOnLiquidEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file 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..023381d7 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt @@ -0,0 +1,78 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import cn.coostack.cooparticlesapi.barrages.HitBox +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 +import kotlin.random.Random + +class BoxEmittersShootType(val box: HitBox) : EmittersShootType { + companion object { + @JvmStatic + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, type -> + type as BoxEmittersShootType + val box = type.box + buf.writeDouble(box.x1) + buf.writeDouble(box.y1) + buf.writeDouble(box.z1) + buf.writeDouble(box.x2) + buf.writeDouble(box.y2) + buf.writeDouble(box.z2) + + }, { + val box = HitBox( + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + ) + BoxEmittersShootType(box) + } + ) + const val ID = "box" + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } + + val random = Random(System.currentTimeMillis()) + override fun getPositions(origin: Vec3, tick: Int, count: Int): List { + return List(count) { + val x = random.nextDouble( + box.x1, + box.x2 + ) + val y = random.nextDouble( + box.y1, + box.y2 + ) + val z = random.nextDouble( + box.z1, + box.z2 + ) + origin.add(Vec3(x, y, z)) + } + } + + override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { + if (enter.length() < 1e-7) { + // 随机速度 + val p = Vec3( + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0) + ) + return p + } + return enter + } +} \ No newline at end of file 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..90a473ad --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 + +interface EmittersShootType { + /** + * 获取id 用于序列化 + */ + fun getID(): String + + /** + * 用于序列化发射类型 + */ + fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + + /** + * 获取生成粒子的位置 + * @param origin 添加offset后的位置 + * @param tick 发射器当前tick + * @param count 粒子数量 + * @return 可能返回多个粒子 + */ + fun getPositions(origin: Vec3, tick: Int, count: Int): List + + /** + * 获取粒子的初始方向 + * @param enter 输入的参数方向 + * @param pos 粒子生成位置 + * @param origin 粒子发射器的位置 + */ + fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 +} \ No newline at end of file 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..dc25731f --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt @@ -0,0 +1,74 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import cn.coostack.cooparticlesapi.barrages.HitBox +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 + +object EmittersShootTypes { + + private val types = HashMap>() + + fun register(id: String, codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec) { + types[id] = codec + } + + fun fromID(id: String) = types[id] + + @JvmStatic + fun point(): EmittersShootType = PointEmittersShootType() + + @JvmStatic + fun box(box: HitBox): EmittersShootType = BoxEmittersShootType(box) + + @JvmStatic + fun line(dir: Vec3, step: Double): EmittersShootType = LineEmittersShootType(dir, step) + + /** + * @param xe 粒子生成 x 相对于发射器的位置偏移量表达式 + * @param ye 粒子生成 y 相对于发射器的位置偏移量表达式 + * @param ze 粒子生成 z 相对于发射器的位置偏移量表达式 + * + * @param dxe 粒子生成时 初始速度向量x表达式 + * @param dye 粒子生成时 初始速度向量y表达式 + * @param dze 粒子生成时 初始速度向量z表达式 + */ + @JvmStatic + fun math( + xe: String, + ye: String, + ze: String, + dxe: String, + dye: String, + dze: String, + ): EmittersShootType = MathEmittersShootType() + .apply { + x = xe + y = ye + z = ze + dx = dxe + dy = dye + dz = dze + setup() + } + + init { + register( + PointEmittersShootType.ID, PointEmittersShootType.CODEC + ) + register( + BoxEmittersShootType.ID, + BoxEmittersShootType.CODEC + ) + register( + MathEmittersShootType.ID, + MathEmittersShootType.CODEC + ) + register( + LineEmittersShootType.ID, + LineEmittersShootType.CODEC + ) + } + + fun init() {} +} \ No newline at end of file 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..1f468c38 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt @@ -0,0 +1,53 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 + + +class LineEmittersShootType(val dir: Vec3, val step: Double) : EmittersShootType { + companion object { + @JvmStatic + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, type -> + type as LineEmittersShootType + buf.writeVec3(type.dir) + buf.writeDouble(type.step) + + }, { + val dir = it.readVec3() + val step = it.readDouble() + LineEmittersShootType(dir, step) + } + ) + const val ID = "line" + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } + + override fun getPositions( + origin: Vec3, + tick: Int, + count: Int + ): List { + return List(count) { + origin.add(dir.normalize().scale(it * step)) + } + } + + override fun getDefaultDirection( + enter: Vec3, + tick: Int, + pos: Vec3, + origin: Vec3 + ): Vec3 { + return enter + } +} \ No newline at end of file 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..a13aaec6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt @@ -0,0 +1,196 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import com.ezylang.evalex.Expression +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 +import java.math.BigDecimal +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.FutureTask + +/** + * 基于EvalEx 表达式的运算 + * 支持的变量 + * 对于粒子生成位置 + * t -> tick + * c -> count + * i -> 当前索引值 + * 对于粒子初始速度 + * t -> tick + * x -> posX + * y -> posY + * z -> posZ + * ox -> origin.x + * oy -> origin.y + * oz -> origin.z + */ +class MathEmittersShootType : EmittersShootType { + companion object { + @JvmStatic + val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { buf, type -> + type as MathEmittersShootType + buf.writeUtf(type.x) + buf.writeUtf(type.y) + buf.writeUtf(type.z) + buf.writeUtf(type.dx) + buf.writeUtf(type.dy) + buf.writeUtf(type.dz) + }, { + MathEmittersShootType().apply { + x = it.readUtf() + y = it.readUtf() + z = it.readUtf() + dx = it.readUtf() + dy = it.readUtf() + dz = it.readUtf() + setup() + } + } + ) + const val ID = "math" + } + + /** + * 位置运算表达式 + */ + var x = "0" + var y = "0" + var z = "0" + + private var xe = Expression(x) + .with("i", 0) + .and("c", 0) + .and("t", 0) + private var ye = Expression(y) + .with("i", 0) + .and("c", 0) + .and("t", 0) + private var ze = Expression(z) + .with("i", 0) + .and("c", 0) + .and("t", 0) + + /** + * 初始方向表达式 + */ + var dx = "0" + var dy = "0" + var dz = "0" + + private var dxe = Expression(dx) + .with("t", 0) + + private var dye = Expression(dy) + .with("t", 0) + + private var dze = Expression(dz) + .with("t", 0) + + fun setup() { + xe = Expression(x) + .with("i", 0) + .and("c", 0) + .and("t", 0) + ye = Expression(y) + .with("i", 0) + .and("c", 0) + .and("t", 0) + ze = Expression(z) + .with("i", 0) + .and("c", 0) + .and("t", 0) + dxe = Expression(dx) + .with("t", 0) + + dye = Expression(dy) + .with("t", 0) + + dze = Expression(dz) + .with("t", 0) + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } + + override fun getPositions( + origin: Vec3, + tick: Int, + count: Int + ): List { + + return List(count) { + val taskX = CompletableFuture.supplyAsync { + xe.with("t", tick) + .with("c", count) + .with("i", it) + .evaluate().numberValue.toDouble() + } + + val taskY = CompletableFuture.supplyAsync { + ye.with("t", tick) + .with("c", count) + .with("i", it) + .evaluate().numberValue.toDouble() + } + + val taskZ = CompletableFuture.supplyAsync { + ze.with("t", tick) + .and("c", count) + .and("i", it) + .evaluate().numberValue.toDouble() + } + CompletableFuture.allOf(taskX, taskY, taskZ).join() + origin.add(Vec3(taskX.get(), taskY.get(), taskZ.get())) + } + } + + override fun getDefaultDirection( + enter: Vec3, + tick: Int, + pos: Vec3, + origin: Vec3 + ): Vec3 { + val x = CompletableFuture.supplyAsync { + dxe.with("t", tick) + .and("x", pos.x) + .and("y", pos.y) + .and("z", pos.z) + .and("ox", origin.x) + .and("oy", origin.y) + .and("oz", origin.z) + .evaluate().numberValue.toDouble() + } + val y = CompletableFuture.supplyAsync { + dye.with("t", tick) + .and("x", pos.x) + .and("y", pos.y) + .and("z", pos.z) + .and("ox", origin.x) + .and("oy", origin.y) + .and("oz", origin.z) + .evaluate().numberValue.toDouble() + } + val z = CompletableFuture.supplyAsync { + dze.with("t", tick) + .and("x", pos.x) + .and("y", pos.y) + .and("z", pos.z) + .and("ox", origin.x) + .and("oy", origin.y) + .and("oz", origin.z) + .evaluate().numberValue.toDouble() + } + CompletableFuture.allOf(x, y, z).join() + return enter.add( + x.get(), y.get(), z.get() + ) + } +} \ No newline at end of file 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..040a9a18 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt @@ -0,0 +1,48 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.PacketByteBuf + +import net.minecraft.world.phys.Vec3 +import kotlin.random.Random + +class PointEmittersShootType : EmittersShootType { + val random = Random(System.currentTimeMillis()) + + companion object { + @JvmStatic + val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + { b, p -> }, { + PointEmittersShootType() + } + ) + const val ID = "point" + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + return CODEC + } + + override fun getPositions(origin: Vec3, tick: Int, count: Int): List { + return List(count) { + origin + } + } + + override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { + if (enter.length() < 1e-7) { + // 随机速度 + val p = Vec3( + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0) + ) + return p + } + return enter + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt new file mode 100644 index 00000000..f98bbc30 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt @@ -0,0 +1,573 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C +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.cparticle.CParticleSystem +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleDisplayer +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.particles.control.ControlParticleManager +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.particles.control.ParticleControler +import cn.coostack.cooparticlesapi.particles.control.RemoveReason +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.set +import kotlin.math.PI + +/** 客户端渲染和服务端处理都用这个类 */ +@Deprecated("使用ParticleComposition") +abstract class ParticleGroupStyle(var visibleRange: Double = 32.0, val uuid: UUID = UUID.randomUUID()) : + Controlable, ServerControler, Tickable { + var world: Level? = null + var pos: Vec3 = Vec3.ZERO + var client = false + var rotate = 0.0 + var axis = RelativeLocation.yAxis() + var scale = 1.0 + set(value) { + field = value.coerceAtLeast(0.001) + } + + var positionDirty = false + + + /** + * 上一次更新的游戏时间 + * > 用来解决回放的时候会产生额外的粒子 或者导致缺少粒子 (额外tick) + */ + + var lastUpdatedGameTime = 0L + + /** 生成粒子样式的时间 可能会直接跳转到其他时候 然后这个时候这个参数就需要进行同步 */ + var displayedTime = 0L + + /** 自动发包同步到客户端 可能会占据大量带宽 */ + var autoToggle = false + + var displayed = false + internal set + internal var valid = true + internal val invokeQueue = ArrayList Unit>() + internal val postInvokeQueue = ArrayList Unit>() + val particles = ConcurrentHashMap>() + val particleLocations = ConcurrentHashMap, RelativeLocation>() + private val cParticleSystems = LinkedHashMap() + private val cParticleContainers = LinkedHashSet>() + + internal fun getCParticleSystems(): List { + removeReleasedCParticleSystems() + return cParticleSystems.keys.toList() + } + + internal fun getCParticleContainers(): List> = + cParticleContainers.toList() + + protected fun prepareCParticleDisplayer(displayer: ParticleDisplayer, capacity: Int) { + if (displayer is CParticleDisplayer) { + displayer.bindDedicatedSystemIfAbsent("style/$uuid", capacity, pos) + } + } + + protected fun registerCParticleNode(controler: Controlable<*>) { + removeReleasedCParticleSystems() + when (controler) { + is CParticleControlable -> cParticleSystems.merge(controler.system, 1, Int::plus) + is ParticleComposition, + is ParticleGroupStyle, + is ControlableParticleGroup -> cParticleContainers.add(controler) + } + } + + protected fun unregisterCParticleNode(controler: Controlable<*>) { + when (controler) { + is CParticleControlable -> { + val count = cParticleSystems[controler.system] ?: return + if (count <= 1) cParticleSystems.remove(controler.system) + else cParticleSystems[controler.system] = count - 1 + } + + is ParticleComposition, + is ParticleGroupStyle, + is ControlableParticleGroup -> cParticleContainers.remove(controler) + } + } + + private fun removeReleasedCParticleSystems() { + cParticleSystems.keys.removeIf { it.released } + } + + override fun isValid(): Boolean { + return valid + } + + /** 当粒子组合初始化时, 存储1倍缩放粒子组与原点的距离 */ + val particleDefaultLength = ConcurrentHashMap() + + + abstract fun getCurrentFrames(): Map + + abstract fun onDisplay() + + /** + * 服务器同步到客户端时, 执行的代码 基类已经存储的参数 如 pos, world ,rotate, axis, scale, uuid 无需同步 + * 自定义的其他参数需要同步 + * + * 如果 autoToggle = true 则会在每个tick都会执行发包 反复调用此方法 + */ + abstract fun writePacketArgs(): Map> + + /** + * 客户端接受服务器的同步时, 使用的代码 + * + * 同步内容如下 -> 客户端创建此类时 输入的基本参数 包括了你在 writePacketArgs输入的参数 客户端在类内存在其余更改时 + * 服务器传入的参数 比如自己设定的一些其他参数值 + * + * 无需处理以下参数 pos world rotate axis scale uuid 其余参数自行处理 + */ + abstract fun readPacketArgs(args: Map>) + + open fun beforeDisplay(styles: Map) { + } + + override fun addPreTickAction(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { + invokeQueue.add(action) + return this + } + + override fun addPreTickActionPost(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { + postInvokeQueue.add(action) + return this + } + + override fun controlUUID(): UUID { + return uuid + } + + override fun rotateToPoint(to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + particleLocations.values.toList(), to, axis + ) + axis = to + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to) + ) + ) + } + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { +// Math3DUtil.rotateAsAxis( +// particleLocations.values.toList(), axis, angle +// ) +// Math3DUtil.rotatePointsToPoint( +// particleLocations.values.toList(), to, axis +// ) + Math3DUtil.rotateToWithRoll( + particleLocations.values.toList(), axis, to, radian + ) + axis = to + this.rotate += radian + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to), + "rotate_angle" to ParticleControlerDataBuffers.double(radian) + ) + ) + } + } + + override fun rotateAsAxis(angle: Double) { + Math3DUtil.rotateAsAxis( + particleLocations.values.toList(), axis, angle + ) + this.rotate += angle + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "rotate_angle" to ParticleControlerDataBuffers.double(angle) + ) + ) + } + } + + override fun teleportTo(pos: Vec3) { + this.pos = pos + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "teleport" to ParticleControlerDataBuffers.vec3d(pos) + ) + ) + } + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + override fun remove() { + clear(false) + if (!client) { + // 同步到其他客户端 + ParticleStyleManager.filterVisiblePlayer(this).forEach { + val player = world!!.getPlayerByUUID(it) as? ServerPlayer + player ?: return@forEach + CooParticlesServices.SERVER_NETWORK.send( + PacketParticleStyleS2C( + uuid, ControlType.REMOVE, mapOf() + ), player + ) + } + } + } + + /** Controler需求 */ + override fun getControlObject(): ParticleGroupStyle { + return this + } + + /** ServerControler需求 */ + override fun getValue(): ParticleGroupStyle { + return this + } + + /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ + fun change(toggleMethod: ParticleGroupStyle.() -> Unit, args: Map>) { + if (client) { + return + } + toggleMethod(this) + // 发包 + ParticleStyleManager.filterVisiblePlayer(this).forEach { + val player = world!!.getPlayerByUUID(it) ?: return@forEach + CooParticlesServices.SERVER_NETWORK.send( + PacketParticleStyleS2C( + uuid, ControlType.CHANGE, args + ), player as ServerPlayer + ) + } + } + + /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ + fun change(args: Map>) { + change({}, args) + } + + open fun display(pos: Vec3, world: Level) { + if (displayed) { + return + } + displayed = true + this.displayedTime = world.gameTime + this.lastUpdatedGameTime = world.gameTime + this.pos = pos + this.world = world + this.client = world.isClientSide + if (!client) { + // 服务器只负责数据同步 不负责粒子生成 + onDisplay() + return + } + flush() + onDisplay() + } + + open fun flush() { + if (particles.isNotEmpty()) { + clear(true) + } + displayParticles() + } + + open fun toggleRelative() { + if (!client) { + return + } + val iterator = particleLocations.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val particle = entry.key + val rl = entry.value + particle.teleportTo( + pos.add( + rl.toVector() + ) + ) + } + positionDirty = false + } + + + private fun spawnSingleParticle(data: StyleData, rel: RelativeLocation) { + val displayer = data.displayerBuilder(data.uuid) + if (displayer !is ParticleDisplayer.SingleParticleDisplayer) return + val controler = ControlParticleManager.createControl(data.uuid) + controler.applyInitializedAction(data.particleHandler) + val toPos = Vec3(pos.x + rel.x, pos.y + rel.y, pos.z + rel.z) + displayer.display(toPos, world as ClientLevel) ?: return + data.particleControlerHandler(controler) + // 把粒子丢回生成列表 + particles[data.uuid] = controler + particleLocations[controler] = rel + } + + override fun tick() { + if (!displayed || !valid) { + clear(false) + return + } + val current = world!!.gameTime + if (!displayed || !valid) { + clear(false) + return + } + invokeQueue.forEach { + it(this) + } + + val iterator = particles.iterator() + while (iterator.hasNext()) { + val style = iterator.next() + when (val value = style.value) { + is Tickable<*> -> { + value.tick() + } + } + } + postInvokeQueue.forEach { + it(this) + } + this.lastUpdatedGameTime = current + } + + fun toggleScale(locations: Map) { + if (!valid) { + // remove过后 无法同步 + return + } + if (particleDefaultLength.isEmpty()) { + locations.forEach { + val uuid = it.key.uuid + particleDefaultLength[uuid] = it.value.length() + } + } + locations.forEach { + val uuid = it.key.uuid + val len = particleDefaultLength[uuid] ?: return@forEach + if (len <= 0.0) { + return@forEach + } + val value = it.value + value.multiply(len * scale / value.length()) + } + } + + open fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale can not be less than zero") + return + } + scale = new + // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 + // 多为服务端(除非有人使在Client环境创建了这个类) + if (displayed) { + toggleScaleDisplayed() + } + // 发包有效 + if (!valid) { + // remove过后 无法同步 + return + } + if (!client) { + change(mapOf("scale" to ParticleControlerDataBuffers.double(new))) + } + } + + protected open fun toggleScaleDisplayed() { + particleLocations.forEach { + val uuid = it.key.controlUUID() + val len = particleDefaultLength[uuid]!! + val value = it.value + if (len in -1e-3..1e-3) return@forEach + value.multiply(len * scale / value.length()) + } + toggleRelative() + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ServerLevel) return + ParticleStyleManager.spawnStyle(world, pos, this) + } + + open fun preRotateTo(map: Map, to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + map.values.toList(), to, axis + ) + this.axis = to + } + + open fun preRotateAsAxis(map: Map, axis: RelativeLocation, angle: Double) { + Math3DUtil.rotateAsAxis( + map.values.toList(), axis, angle + ) + this.axis = axis + } + + open fun preRotateAsAxis(map: Map, angle: Double) { + Math3DUtil.rotateAsAxis( + map.values.toList(), axis, angle + ) + } + + private fun displayParticles() { + val locations = getCurrentFrames() + beforeDisplay(locations) + toggleScale(locations) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) + locations.forEach { + val data = it.key + val uuid = it.key.uuid + val rl = it.value + val displayer = it.key.displayerBuilder(uuid) + prepareCParticleDisplayer(displayer, locations.size) + if (displayer is ParticleDisplayer.SingleParticleDisplayer) { + val controler = ControlParticleManager.createControl(uuid) + controler.applyInitializedAction(data.particleHandler) + } + val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) + val controler = displayer.display(toPos, world as ClientLevel) ?: return@forEach + if (controler is ParticleControler) { + data.particleControlerHandler(controler) + } + registerCParticleNode(controler) + + particles[uuid] = controler + particleLocations[controler] = rl + } + } + + override fun remove(reason: RemoveReason) { + remove() + } + + open internal fun clear(valid: Boolean) { + particles.forEach { + it.value.remove() + } + particles.clear() + particleLocations.clear() + particleDefaultLength.clear() + cParticleSystems.clear() + cParticleContainers.clear() + this.valid = valid + } + + open class StyleData( + val displayerBuilder: (UUID) -> ParticleDisplayer, + ) { + val uuid: UUID = UUID.randomUUID() + var particleHandler: ControlableParticle.() -> Unit = {} + var particleControlerHandler: ParticleControler.() -> Unit = {} + fun withParticleHandler( + builder: ControlableParticle.() -> Unit + ): StyleData { + particleHandler = builder + return this + } + + fun withParticleControlerHandler( + builder: ParticleControler.() -> Unit + ): StyleData { + particleControlerHandler = builder + return this + } + } + + /** 为了提高傻逼StyleData的复用性 专门设置此类 */ + open class StyleDataBuilder() { + private var displayerBuilder: (UUID) -> ParticleDisplayer = + { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } + private val particleHandlers = mutableListOf Unit>() + private val particleControlerHandlers = mutableListOf Unit>() + fun addParticleHandler( + builder: ControlableParticle.() -> Unit + ): StyleDataBuilder { + particleHandlers.add(builder) + return this + } + + fun addParticleControlerHandler( + builder: ParticleControler.() -> Unit + ): StyleDataBuilder { + particleControlerHandlers.add(builder) + return this + } + + fun clearParticleHandlers(): StyleDataBuilder { + particleHandlers.clear() + return this + } + + fun clearParticleControlers(): StyleDataBuilder { + particleControlerHandlers.clear() + return this + } + + fun removeHandler(index: Int): StyleDataBuilder { + if (index in particleHandlers.indices) particleHandlers.removeAt(index) + return this + } + + fun removeParticleControler(index: Int): StyleDataBuilder { + if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) + return this + } + + fun displayer(builder: (UUID) -> ParticleDisplayer): StyleDataBuilder { + this.displayerBuilder = builder + return this + } + + fun build(): StyleData = StyleData(displayerBuilder) + .withParticleHandler { + particleHandlers.forEach { it() } + }.withParticleControlerHandler { + particleControlerHandlers.forEach { it() } + } + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt new file mode 100644 index 00000000..25e9c85d --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt @@ -0,0 +1,222 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper +import net.minecraft.client.particle.ParticleRenderType +import net.minecraft.world.entity.player.Player +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +/** + * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) + * 专门创建此类 + * + * 此类没有Provider 因此无法单独生成 + * + * 继承此类的目的是为这个类创建一个专属形状的子类 + * 这种子类可以拥有Provider + */ +open class ParticleShapeStyle(uuid: UUID) : + ParticleGroupStyle(64.0, uuid) { + var scaleHelper: ScaleHelper? = null + + /** + * 增加可复用性 + */ + private val displayInvokes = mutableListOf Unit>() + private val beforeDisplayInvokes = mutableListOf) -> Unit>() + private val pointBuilders = LinkedHashMap StyleData>() + + /** + * 通过判断(外层) 提供的状态工具来决定是否反转缩放 + * 在toggleDisplay执行这个方法 + */ + val reverseFunctionFromStatus: (ParticleShapeStyle, StyleStatusHelper) -> Unit = function@{ it, displayStatus -> + // 必须设置缩放工具 + if (scaleHelper == null) return@function + it.addPreTickAction { + if (it.scaleReversed && it.scaleHelper!!.isZero()) { + it.remove() + return@addPreTickAction + } + if (displayStatus.displayStatus != 2) { + return@addPreTickAction + } + if (!it.scaleReversed) { + it.scaleReversed(false) + } + } + } + var spawnAge = 0 + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + private set + + fun appendBuilder(pointsBuilder: PointsBuilder, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { + pointBuilders[pointsBuilder] = dataBuilder + return this + } + + fun appendPoint(point: RelativeLocation, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { + pointBuilders[ + PointsBuilder().also { it.addPoint(point) } + ] = dataBuilder + return this + } + + /** + * @param max 直接让scale从最大开始 + */ + fun scaleReversed(max: Boolean): ParticleShapeStyle { + scaleHelper ?: return this + scaleReversed = true + if (max) { + scaleHelper!!.toggleScale(scaleHelper!!.maxScale) + } + return this + } + + fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): ParticleShapeStyle { + scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): ParticleShapeStyle { + scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + /** + * 在display之前执行 + * 可以多次执行此方法, 不会发生方法覆盖 + */ + fun toggleOnDisplay(toggleMethod: ParticleShapeStyle.() -> Unit): ParticleShapeStyle { + displayInvokes.add(toggleMethod) + return this + } + + /** + * 在生成粒子之前执行 + */ + fun toggleBeforeDisplay(toggleMethod: ParticleShapeStyle.(Map) -> Unit): ParticleShapeStyle { + beforeDisplayInvokes.add(toggleMethod) + return this + } + + override fun beforeDisplay(styles: Map) { + beforeDisplayInvokes.forEach { it(styles) } + } + + override fun getCurrentFrames(): Map { + val res = HashMap() + pointBuilders.forEach { entry -> + res.putAll(entry.key.createWithStyleData { entry.value(it) }) + } + return res + } + + override fun isValid(): Boolean { + return valid + } + + override fun onDisplay() { + displayInvokes.forEach { + it() + } + addPreTickAction { + spawnAge++ + if (!scalePreTick || scaleHelper == null) { + return@addPreTickAction + } + + if (scaleReversed) { + scaleHelper!!.doScaleReversed() + } else { + scaleHelper!!.doScale() + } + } + } + + override fun writePacketArgs(): Map> { + return mapOf() + } + + override fun readPacketArgs(args: Map>) { + } + + fun fastRotateToPlayerView(player: Player) { + rotateToPoint(RelativeLocation.of(player.forward)) + } + + fun fastStyleData(color: Vec3, displayer: (UUID) -> ParticleDisplayer): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + } + } + + fun fastStyleData(color: Vec3, sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + } + } + + fun fastStyleData( + color: Vec3, + sheet: ParticleRenderType, + size: Float, + displayer: (UUID) -> ParticleDisplayer + ): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + } + } + + fun fastStyleData( + color: Vec3, + sheet: ParticleRenderType, + size: Float, + alpha: Float, + displayer: (UUID) -> ParticleDisplayer + ): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + this.particleAlpha = alpha + } + } + + fun fastStyleData(sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { + return StyleData(displayer).withParticleHandler { + this.textureSheet = sheet + } + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt new file mode 100644 index 00000000..dd68ba0c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt @@ -0,0 +1,259 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.client.Minecraft +import net.minecraft.server.level.ServerPlayer +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 + +/** + * TODO 重构Style使得让自动更新默认打开(如果更新时发现不存在那就重新生成) + * + * @constructor Create empty Particle style manager + */ +@Deprecated("使用ParticleComposition") +object ParticleStyleManager { + + /** + * 服务端拥有的 -> (Server View)客户层是empty + */ + val serverViewStyles = ConcurrentHashMap() + + /** + * 玩家可以看见的 style + */ + internal val visible = ConcurrentHashMap>() + + /** + * 当前客户端可见的 (Client View)服务层是empty + */ + val clientViewStyles = ConcurrentHashMap() + + private val registerBuilders = + HashMap, ParticleStyleProvider>() + + /** + * 在ClientModInitializer 注册ParticleStyle 用于服务器同步 + */ + // Keep this as an instance method for binary compatibility with existing consumers. + fun register( + type: Class, + provider: ParticleStyleProvider + ) { + registerBuilders[type] = provider + } + + fun getBuilder(type: Class): ParticleStyleProvider? { + return registerBuilders[type] + } + + @JvmStatic + fun spawnStyle(world: Level, pos: Vec3, style: ParticleGroupStyle) { + if (world.isClientSide) { + // 生成粒子 + style.display(pos, world) + clientViewStyles[style.uuid] = style + return + } + style.display(pos, world) + serverViewStyles[style.uuid] = style + // 发送数据包 -> 包括type + world.players().filter { style.pos.distanceTo(it.position()) <= style.visibleRange }.forEach { + addStylePlayerView(it as ServerPlayer, style) + } + } + + + fun doTickClient() { + val player = Minecraft.getInstance().player ?: return + if (player.isDeadOrDying) { + clientViewStyles.clear() + return + } + val iterator = clientViewStyles.iterator() + while (iterator.hasNext()) { + val style = iterator.next().value + style.tick() + if (!style.valid) { + iterator.remove() + } + } + } + + fun doTickServer() { + val iterator = serverViewStyles.iterator() + while (iterator.hasNext()) { + val style = iterator.next().value + // 更新可见性 + upgradeVisible(style) + style.tick() + if (style.autoToggle) { + style.world!!.players().forEach { + val visibleSet = visible.getOrPut(it.uuid) { HashSet() } + // 不可见的粒子没有必要同步 + if (!visibleSet.contains(style)) { + return@forEach + } + CooParticlesServices.SERVER_NETWORK.send( + buildAutoTogglePacket(style), + it as ServerPlayer, + ) + } + } + if (!style.valid) { + filterVisiblePlayer(style).forEach { + val player = style.world!!.getPlayerByUUID(it) ?: return@forEach + removeGroupPlayerView(player as ServerPlayer, style) + visible[it]?.remove(style) + } + iterator.remove() + } + } + val playerVisibleIterator = visible.iterator() + while (playerVisibleIterator.hasNext()) { + val playerUUID = playerVisibleIterator.next().key + // 判断玩家是否在线 + val server = CooParticlesAPI.serverOrNull ?: return + val player = server.playerList.getPlayer(playerUUID) + if (player == null) { + playerVisibleIterator.remove() + } + } + } + + fun filterVisiblePlayer(group: ParticleGroupStyle): Set { + val set = HashSet() + visible.forEach { + if (group in it.value) { + set.add(it.key) + } + } + return set + } + + private fun upgradeVisible(style: ParticleGroupStyle) { + val server = CooParticlesAPI.serverOrNull ?: return + server.playerList.players.forEach { p -> + val visibleSet = visible.getOrPut(p.uuid) { HashSet() } + if (p.level().dimension() != style.world?.dimension()) { + // 世界转换 + if (style in visibleSet) { + removeGroupPlayerView(p, style) + visibleSet!!.remove(style) + } + return@forEach + } + if (p.isDeadOrDying) { + if (style in visibleSet) { + removeGroupPlayerView(p, style) + visibleSet!!.remove(style) + } + return@forEach + } + if (style.pos.distanceTo(p.position()) <= style.visibleRange) { + // 防止重复添加(发包) + if (style in visibleSet) { + return@forEach + } + addStylePlayerView(p, style) + } else { + if (style in visibleSet) { + removeGroupPlayerView(p, style) + } + // 超过范围 + visibleSet.remove(style) + } + } + } + + /** + * 在该玩家的视角中移除这些粒子 + * @param target + * @param targetGroup + */ + private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ParticleGroupStyle) { + // 发包给玩家 + val packet = PacketParticleStyleS2C( + targetGroup.uuid, + ControlType.REMOVE, mapOf() + ) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + /* + * @param target + * @param targetStyle + */ + private fun addStylePlayerView( + target: ServerPlayer, + targetStyle: ParticleGroupStyle, + ) { + // 修复一个包发送给一个玩家两次 + val visibleSet = visible.getOrPut(target.uuid) { HashSet() } + visibleSet.add(targetStyle) + // 发包给玩家 + val packet = buildCreatePacket(targetStyle, targetStyle.pos) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + private fun buildAutoTogglePacket( + style: ParticleGroupStyle, + ): PacketParticleStyleS2C { + + /** + * SequencedParticleStyle + * 你赢了 + * addMultiple的index 能从 7 自动同步成 0 + * + * 一看 server side 诶 index没有问题 + * 一看args 诶 index没有问题 + * 但是呢 + * 传入客户端之后啊 + * 7 变成 0 了捏 + * 不知道的以为是Packet的 CODEC被你妈吃数据包了 + */ + + return PacketParticleStyleS2C( + style.uuid, + ControlType.CHANGE, + mapOf( + *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() + ) + ) + } + + private fun buildCreatePacket(style: ParticleGroupStyle, pos: Vec3): PacketParticleStyleS2C = + PacketParticleStyleS2C( + style.uuid, + ControlType.CREATE, + mapOf( + "style_type" to ParticleControlerDataBuffers.string(style::class.java.name), + "pos" to ParticleControlerDataBuffers.vec3d(pos), + "rotate" to ParticleControlerDataBuffers.double(style.rotate), + "axis" to ParticleControlerDataBuffers.vec3d(style.axis.toVector()), + "scale" to ParticleControlerDataBuffers.double(style.scale), + "lastUpdatedGameTime" to ParticleControlerDataBuffers.long(style.lastUpdatedGameTime), + "displayedTime" to ParticleControlerDataBuffers.long(style.displayedTime), + *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() + ) + ) + + fun clearAllVisible() { + clientViewStyles.onEach { + it.value.remove() + }.clear() + } + + fun clearServer() { + serverViewStyles.clear() + visible.clear() + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt new file mode 100644 index 00000000..acca17be --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import java.util.UUID + +@Deprecated("使用ParticleComposition") +interface ParticleStyleProvider { + /** + * 创建style + * 执行此方法的时候不要调用基本参数(因为还没初始化) + */ + fun createStyle(uuid: UUID, args: Map>): ParticleGroupStyle +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt new file mode 100644 index 00000000..d8a36a12 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt @@ -0,0 +1,288 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper +import net.minecraft.client.particle.ParticleRenderType +import net.minecraft.world.entity.player.Player +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.TreeMap +import java.util.UUID +import java.util.function.Predicate + +/** + * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) + * 专门创建此类 + * + * 无法单独生成 + * 继承此类的目的是为这个类创建一个专属形状的子类 + * client属性始终为true + */ +class SequencedParticleShapeStyle(uuid: UUID) : + SequencedParticleStyle(64.0, uuid) { + var scaleHelper: ScaleHelper? = null + + private val displayInvokes = mutableListOf Unit>() + private val beforeDisplayInvokes = + mutableListOf) -> Unit>() + + private val pointBuilders = LinkedHashMap SortedStyleData>() + + /** + * 用于控制粒子的播放顺序 + * 如果满足 animationConditions[animationIndex].first的条件 + * 则会执行 addMultiple(second (second > 0)) + * 或者 removeMultiple(second (second < 0) + * 符合条件后 animationIndex 会递增 + * 直到到达边界 + */ + private val animationConditions = ArrayList, Int>>() + var animationIndex = 0 + private set + + /** + * 通过判断(外层) 提供的状态工具来决定是否反转缩放 + * 在toggleDisplay执行这个方法 + */ + val reverseFunctionFromStatus: (SequencedParticleShapeStyle, StyleStatusHelper) -> Unit = + function@{ it, displayStatus -> + // 必须设置缩放工具 + if (scaleHelper == null) return@function + it.addPreTickAction { + if (it.scaleReversed && it.scaleHelper!!.isZero()) { + it.remove() + return@addPreTickAction + } + if (displayStatus.displayStatus != 2) { + return@addPreTickAction + } + if (!it.scaleReversed) { + it.scaleReversed(false) + } + } + } + var spawnAge = 0 + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + private set + + fun appendBuilder( + pointsBuilder: PointsBuilder, + dataBuilder: (RelativeLocation) -> SortedStyleData + ): SequencedParticleShapeStyle { + pointBuilders[pointsBuilder] = dataBuilder + return this + } + + fun appendPoint( + point: RelativeLocation, + dataBuilder: (RelativeLocation) -> SortedStyleData + ): SequencedParticleShapeStyle { + pointBuilders[ + PointsBuilder().also { it.addPoint(point) } + ] = dataBuilder + return this + } + + /** + * @param predicate 设置添加/删除动画的要求 + * @param add 向下的个数 + */ + fun appendAnimateCondition( + predicate: Predicate, + add: Int + ): SequencedParticleShapeStyle { + animationConditions.add(predicate to add) + return this + } + + /** + * @param max 直接让scale从最大开始 + */ + fun scaleReversed(max: Boolean): SequencedParticleShapeStyle { + scaleHelper ?: return this + scaleReversed = true + if (max) { + scaleHelper!!.toggleScale(scaleHelper!!.maxScale) + } + return this + } + + fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): SequencedParticleShapeStyle { + scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): SequencedParticleShapeStyle { + scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + /** + * 在display之前执行 + */ + fun toggleOnDisplay(toggleMethod: SequencedParticleShapeStyle.() -> Unit): SequencedParticleShapeStyle { + displayInvokes.add(toggleMethod) + return this + } + + /** + * 在生成粒子之前执行 + */ + fun toggleBeforeDisplay(toggleMethod: SequencedParticleShapeStyle.(SortedMap) -> Unit): SequencedParticleShapeStyle { + beforeDisplayInvokes.add(toggleMethod) + return this + } + + + override fun beforeDisplay(styles: SortedMap) { + beforeDisplayInvokes.forEach { + it(styles) + } + } + + + override fun onDisplay() { + displayInvokes.forEach { it() } + addPreTickAction { + if (animationIndex >= animationConditions.size) { + return@addPreTickAction + } + val (predicate, add) = animationConditions[animationIndex] + if (predicate.test(this@SequencedParticleShapeStyle)) { + if (add > 0) { + addMultiple(add) + } else { + removeMultiple(add) + } + animationIndex++ + } + } + addPreTickAction { + spawnAge++ + if (!scalePreTick || scaleHelper == null) { + return@addPreTickAction + } + + if (scaleReversed) { + scaleHelper!!.doScaleReversed() + } else { + scaleHelper!!.doScale() + } + } + } + + private var count = -1 + override fun getParticlesCount(): Int { + // 调用此方法比初始化count早 + if (count == -1 || count == 0) { + count = getCurrentFramesSequenced().size + } + return count + } + + override fun getCurrentFramesSequenced(): SortedMap { + val res = TreeMap() + pointBuilders.forEach { entry -> + res.putAll(entry.key.createWithSequencedStyleData { it, order -> + entry.value(it) + }) + } + return res + } + + override fun writePacketArgsSequenced(): Map> { + return mapOf() + } + + override fun readPacketArgsSequenced(args: Map>) { + } + + + fun fastRotateToPlayerView(player: Player) { + rotateToPoint(RelativeLocation.of(player.forward)) + } + + fun fastStyleData(order: Int, color: Vec3, displayer: (UUID) -> ParticleDisplayer): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + color: Vec3, + sheet: ParticleRenderType, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + color: Vec3, + sheet: ParticleRenderType, + size: Float, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + color: Vec3, + sheet: ParticleRenderType, + size: Float, + alpha: Float, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + this.particleAlpha = alpha + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + sheet: ParticleRenderType, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.textureSheet = sheet + } as SortedStyleData + } +} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt new file mode 100644 index 00000000..f9825d8c --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt @@ -0,0 +1,551 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.particles.ControlableParticle +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.impl.ControlableEndRodEffect +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.MathDataUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.UUID +import kotlin.collections.toList +import kotlin.math.PI + + +/** + * 几个免除手动书写 SequencedParticleStyle的方法 + * + * 在这里不建议使用 autoToggle方法 会导致一些奇奇怪怪的问题 (原因未知) 可能是 部分index同步错误导致 + */ +@Deprecated("使用ParticleComposition") +abstract class SequencedParticleStyle(visibleRange: Double = 32.0, uuid: UUID = UUID.randomUUID()) : + ParticleGroupStyle(visibleRange, uuid) { + companion object { + const val CREATE_PARTICLE = 1 + const val DELETE_PARTICLE = 0 + } + + + class SortedStyleData(displayerBuilder: (UUID) -> ParticleDisplayer, val order: Int) : + StyleData(displayerBuilder), Comparable { + override fun compareTo(other: SortedStyleData): Int { + return order - other.order + } + } + + + class SortedStyleDataBuilder { + private var displayerBuilder: (UUID) -> ParticleDisplayer = + { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } + private val particleHandlers = mutableListOf Unit>() + private val particleControlerHandlers = mutableListOf Unit>() + fun addParticleHandler( + builder: ControlableParticle.() -> Unit + ): SortedStyleDataBuilder { + particleHandlers.add(builder) + return this + } + + fun addParticleControlerHandler( + builder: ParticleControler.() -> Unit + ): SortedStyleDataBuilder { + particleControlerHandlers.add(builder) + return this + } + + fun clearParticleHandlers(): SortedStyleDataBuilder { + particleHandlers.clear() + return this + } + + fun clearParticleControlers(): SortedStyleDataBuilder { + particleControlerHandlers.clear() + return this + } + + fun removeHandler(index: Int): SortedStyleDataBuilder { + if (index in particleHandlers.indices) particleHandlers.removeAt(index) + return this + } + + fun removeParticleControler(index: Int): SortedStyleDataBuilder { + if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) + return this + } + + fun displayer(builder: (UUID) -> ParticleDisplayer): SortedStyleDataBuilder { + this.displayerBuilder = builder + return this + } + + fun build(order: Int): SortedStyleData = SortedStyleData(displayerBuilder, order) + .withParticleHandler { + particleHandlers.forEach { it() } + }.withParticleControlerHandler { + particleControlerHandlers.forEach { it() } + } as SortedStyleData + } + + /** + * 在服务器处是0 size的大小 client用于存储某个粒子/粒子组 的展示情况 + * + * 用于彻底解除强制重写的烦恼 + * + * FIXED 修复多余的内存占用 + */ + val displayedStatus: LongArray by lazy { + val count = getParticlesCount() + var page = count / 64 + if (count % 64 > 0) { + page++ + } + LongArray(page) + } + + + /** 统计对应的粒子顺序 (uuid) */ + protected var sequencedParticles = ArrayList>() + private set + + + var displayedParticleCount = 0 + private set(value) { + field = value.coerceAtLeast(0) + } + + /** + * 此参数所指向的状态为false + * + * 服务器可用 不会同步到客户端 + */ + var particleLinkageDisplayCurrentIndex = 0 + private set + + /** + * @return 当前粒子样式的样式个数 (客户端执行getCurrentFramesSequenced返回的集合长度) 请手动计算粒子个数 + * 或者维护一个变量 支持服务器和客户端同时生成 请勿直接在服务器中调用 getCurrentFramesSequenced().size + * 否则会导致服务器崩溃 + */ + abstract fun getParticlesCount(): Int + abstract fun getCurrentFramesSequenced(): SortedMap + abstract fun writePacketArgsSequenced(): Map> + abstract fun readPacketArgsSequenced(args: Map>) + + /** 服务器发包 -> index_status_change -> intArray index, status */ + fun addSingle() { + if (client) { + // 适配在纯客户端页面的添加 + // 由于和服务器无关所以大多数参数无需设置 + if (particleLinkageDisplayCurrentIndex >= getParticlesCount()) { + return + } + toggleFromStatus(particleLinkageDisplayCurrentIndex++, true) + return + } + displayedParticleCount++ + if (getStatus(particleLinkageDisplayCurrentIndex)) { + return + } + setStatus(particleLinkageDisplayCurrentIndex, true) + // 处理服务器 + // 发包 + val args = buildChangeSingleStatusArgs( + particleLinkageDisplayCurrentIndex++, + true + ) + change({}, mapOf(args)) + } + + /** + * 服务器发包 -> indexes_status_change_arg -> intArray indexes + * indexes_status_change_method -> intArray status method + */ + fun addMultiple(count: Int) { + if (client) { + repeat(count) { + addSingle() + } + return + } + if (count <= 0) { + return + } + if (count == 1) { + addSingle() + return + } + displayedParticleCount += count + // 处理服务器 + // 发包 + val indexes = particleLinkageDisplayCurrentIndex..> { + return mapOf( + "indexes_status_change_arg" to ParticleControlerDataBuffers.intArray(indexes), + "indexes_status_change_status" to ParticleControlerDataBuffers.intArray( + intArrayOf(if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount) + ) + ) + } + + private fun buildChangeSingleStatusArgs( + index: Int, + status: Boolean, + ): Pair> { + return "index_status_change" to ParticleControlerDataBuffers.intArray( + intArrayOf( + index, + if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount + ) + ) + } + + open fun beforeDisplay(styles: SortedMap) {} + fun toggleScale(locations: SortedMap) { + super.toggleScale(locations.toMap()) + } + + override fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale can not be less than zero") + return + } + scale = new + if (displayed) { + toggleScaleDisplayed() + } + } + + override fun display(pos: Vec3, world: Level) { + if (displayed) { + return + } + displayed = true + this.pos = pos + this.world = world + this.client = world.isClientSide + if (!client) { + // 服务器只负责数据同步 不负责粒子生成 + onDisplay() + return + } + flush() + toggleDataStatus() + onDisplay() + } + + /** 清空粒子生成状态 重新生成粒子 */ + override fun flush() { + if (particles.isNotEmpty()) { + clear(true) + clearStatus() + } + displayParticles() + } + + override fun clear(valid: Boolean) { + sequencedParticles.clear() + super.clear(valid) + } + + /** 新的参数用于传输 粒子启用顺序 */ + override fun writePacketArgs(): Map> { + return mapOf( + "status" to ParticleControlerDataBuffers.longArray(displayedStatus), + "displayed_particle_count" to ParticleControlerDataBuffers.int(displayedParticleCount), + *writePacketArgsSequenced().map { it.key to it.value }.toTypedArray() + ) + } + + override fun readPacketArgs(args: Map>) { + args["status"]?.let { + val array = it.loadedValue as LongArray + System.arraycopy(array, 0, displayedStatus, 0, array.size) + } + args["displayed_particle_count"]?.let { displayedParticleCount = it.loadedValue!! as Int } + readPacketArgsSequenced(args) + args["index_status_change"]?.let { + val array = it.loadedValue as IntArray + + /** @see particleLinkageDisplayCurrentIndex */ + val index = array[0] + val status = array[1] == CREATE_PARTICLE + this.displayedParticleCount = array[2] + changeSingleStatus(index, status) + } + + args["indexes_status_change_arg"]?.let { + val indexes = it.loadedValue as IntArray + val methodArray = args["indexes_status_change_status"]!!.loadedValue as IntArray + val status = methodArray[0] == CREATE_PARTICLE + this.displayedParticleCount = methodArray[1] + changeParticlesStatus(indexes, status) + } + } + + fun displayParticles() { + val locations = getCurrentFramesSequenced() + beforeDisplay(locations) + toggleScale(locations) + sequencedParticles.addAll(locations.toList()) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) + } + + override fun rotateAsAxis(angle: Double) { + Math3DUtil.rotateAsAxis( + sequencedParticles.map { it.second }.toList(), axis, angle + ) + this.rotate += angle + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client && !autoToggle) { + // 同步到其他客户端 + change( + mapOf( + "rotate_angle" to ParticleControlerDataBuffers.double(angle) + ) + ) + } + } + + override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { + Math3DUtil.rotateAsAxis( + sequencedParticles.map { it.second }.toList(), axis, angle + ) + Math3DUtil.rotatePointsToPoint( + sequencedParticles.map { it.second }.toList(), to, axis + ) + axis = to + this.rotate += angle + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client && !autoToggle) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to), + "rotate_angle" to ParticleControlerDataBuffers.double(angle) + ) + ) + } + } + + override fun rotateToPoint(to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + sequencedParticles.map { it.second }.toList(), to, axis + ) + axis = to + toggleRelative() + if (!client && !autoToggle) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to) + ) + ) + } + } + + final override fun toggleScaleDisplayed() { + sequencedParticles.forEach { + val uuid = it.first.uuid + val len = particleDefaultLength[uuid]!! + val value = it.second + if (len in -1e-3..1e-3) return@forEach + value.multiply(len * scale / value.length()) + } + toggleRelative() + } + + private fun createWithIndex(index: Int) { + if (!client) { + return + } + + if (index !in 0..sequencedParticles.size - 1) { + return + } + + val (data, rl) = sequencedParticles[index] + val uuid = data.uuid + + val displayer = data.displayerBuilder(uuid) + prepareCParticleDisplayer(displayer, sequencedParticles.size) + if (displayer is ParticleDisplayer.SingleParticleDisplayer) { + val controler = ControlParticleManager.createControl(uuid) + controler.applyInitializedAction(data.particleHandler) + } + val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) + val controler = displayer.display(toPos, world as ClientLevel) ?: return + if (controler is ParticleControler) { + data.particleControlerHandler(controler) + } + registerCParticleNode(controler) + particles[uuid] = controler + particleLocations[controler] = rl + } + + private fun toggleFromStatus(index: Int, status: Boolean) { + if (index >= sequencedParticles.size && client || index > getParticlesCount()) return + if (index < 0) return + if (!client) return + if (status) { + createWithIndex(index) + } else { + val uuid = sequencedParticles[index].first.uuid + val particle = particles[uuid] ?: return + unregisterCParticleNode(particle) + particle.remove() + particles.remove(uuid) + particleLocations.remove(particle) + } + setStatus(index, status) + } + + private fun setStatus(index: Int, status: Boolean) { + val page = MathDataUtil.getStoragePageLong(index) + val container = displayedStatus[page] + val bit = MathDataUtil.getStorageWithBitLong(index) + displayedStatus[page] = MathDataUtil.setStatusLong(container, bit, status) + } + + private fun getStatus(index: Int): Boolean { + val page = MathDataUtil.getStoragePageLong(index) + val container = displayedStatus[page] + val bit = MathDataUtil.getStorageWithBitLong(index) + return MathDataUtil.getStatusLong(container, bit) == 1 + } + + /** 在同步了data状态后 执行生成已经生成的 客户端执行 */ + private fun toggleDataStatus() { + if (!client) { + return + } + + if (displayedStatus.isEmpty()) return + if (!displayed) return + displayedStatus.forEachIndexed { page, container -> + for (bit in 1..64) { + val index = page * 64 + bit - 1 + if (index >= sequencedParticles.size) { + break + } + val status = MathDataUtil.getStatusLong(container, bit) + toggleFromStatus(index, status == 1) + } + + } + } + + private fun clearStatus() { + for (i in displayedStatus.indices) { + displayedStatus[i] = 0L + } + } + + final override fun getCurrentFrames(): Map { + return mapOf() + } + + + final override fun beforeDisplay(styles: Map) {} +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt new file mode 100644 index 00000000..428667b6 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt @@ -0,0 +1,111 @@ +package cn.coostack.cooparticlesapi.network.particle.util + +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.server.level.ServerLevel +import net.minecraft.world.phys.Vec3 + +object ServerParticleUtil { + + @JvmStatic + fun spawnBatch( + type: ParticleOptions, + world: ServerLevel, + positions: Iterable, + velocity: Vec3, + range: Double, + ) { + val allPositions = positions.toList() + if (allPositions.isEmpty()) return + world.players().forEach { player -> + val visiblePositions = allPositions.filter { player.position().distanceTo(it) <= range } + visiblePositions.chunked(PacketParticleBatchS2C.MAX_PARTICLES).forEach { batch -> + CooParticlesServices.SERVER_NETWORK.send(PacketParticleBatchS2C(type, batch, velocity), player) + } + } + } + + @JvmStatic + fun spawnBatch( + type: ParticleOptions, + world: ServerLevel, + positions: Iterable, + velocity: Vec3, + ) { + positions.toList() + .chunked(PacketParticleBatchS2C.MAX_PARTICLES) + .filter { it.isNotEmpty() } + .forEach { batch -> + val packet = PacketParticleBatchS2C(type, batch, velocity) + world.players().forEach { CooParticlesServices.SERVER_NETWORK.send(packet, it) } + } + } + /** + * 使用minecraft的 spawnParticle方法 + * 可能无法设置粒子移动方向 + */ + fun spawnSingle( + type: ParticleOptions, + world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int + ) { + world.players().forEach { + world.sendParticles( + it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed + ) + } + } + + /** + * 使用minecraft的 spawnParticle方法 + * 可能无法设置粒子移动方向 + */ + fun spawnSingle( + type: ParticleOptions, + world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int, range: Double + ) { + world.players().forEach { + if (it.position().distanceTo(pos) > range) { + return@forEach + } + world.sendParticles( + it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed + ) + } + } + + /** + * 使用CooParticleAPI的 spawnParticle方法 + */ + fun spawnSingle( + type: ParticleOptions, + world: ServerLevel, + pos: RelativeLocation, + velocity: RelativeLocation, + range: Double + ) { + spawnSingle(type, world, pos.toVector(), velocity.toVector(), range) + } + + fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: RelativeLocation, velocity: RelativeLocation) { + spawnSingle(type, world, pos.toVector(), velocity.toVector()) + } + + fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3, range: Double) { + world.players().forEach { + if (it.position().distanceTo(pos) > range) { + return@forEach + } + CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) + } + } + + fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3) { + world.players().forEach { + CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) + } + } + +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt new file mode 100644 index 00000000..a52de482 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt @@ -0,0 +1,5 @@ +package cn.coostack.cooparticlesapi.platform + +interface ClientNetworking { + fun send(packet: Any) +} 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..545d1dd8 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt @@ -0,0 +1,59 @@ +package cn.coostack.cooparticlesapi.platform + +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeC2S +import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeS2C +import net.minecraft.network.PacketByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraftforge.network.NetworkEvent +import net.minecraftforge.network.simple.SimpleChannel + +class ForgeNetworkChannel(private val modId: String) { + val channel: SimpleChannel = net.minecraftforge.network.NetworkRegistry.newSimpleChannel( + ResourceLocation(modId, "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 sendTo(player: net.minecraft.server.level.ServerPlayer, packet: CooPacket) { + channel.sendTo(player, packet) + } + + fun sendToAll(packet: CooPacket) { + channel.sendToAll(packet) + } + + fun sendToServer(packet: CooPacket) { + channel.sendToServer(packet) + } + + fun sendEnvelopeC2S(packet: CooPacketEnvelopeC2S) { + channel.sendToServer(packet) + } +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt new file mode 100644 index 00000000..984884a9 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.platform + +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.ChunkPos + +interface ServerNetworking { + fun send(packet: Any, to: ServerPlayer) + fun sendAllPlayers(packet: Any) + fun sendToPlayersTrackingChunk(world: ServerLevel, chunk: ChunkPos, packet: Any) + + fun sendToWorld(world: ServerLevel, packet: Any) { + world.players().forEach { send(packet, it) } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt new file mode 100644 index 00000000..0c548747 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt @@ -0,0 +1,40 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class BooleanControlerBuffer() : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "boolean" + ) + ) + } + + override var loadedValue: Boolean? = false + + override fun encode(value: Boolean): ByteArray { + return byteArrayOf(if (value) 1 else 0) + } + + + override fun encode(): ByteArray { + return encode(loadedValue ?: false) + } + + override fun decode(buf: ByteArray): Boolean { + if (buf.isNotEmpty()) { + return buf[0] == 1.toByte() + } + return false + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt new file mode 100644 index 00000000..9fd449e4 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class CharControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "char" + ) + ) + } + override var loadedValue: Char? = ' ' + override fun encode(value: Char): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Char { + return String(buf)[0] + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt new file mode 100644 index 00000000..70e91e64 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class DoubleControlerBuffer() : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "double" + ) + ) + } + override var loadedValue: Double? = 0.0 + override fun encode(value: Double): ByteArray { + return value.toString().toByteArray() + } + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Double { + return String(buf).toDouble() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt new file mode 100644 index 00000000..fa82448f --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt @@ -0,0 +1,36 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class EmptyControlerBuffer() : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "empty" + ) + ) + } + + override var loadedValue: Unit? = null + + override fun encode(value: Unit): ByteArray { + return byteArrayOf(0) + } + + + override fun encode(): ByteArray { + return byteArrayOf() + } + + override fun decode(buf: ByteArray) { + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt new file mode 100644 index 00000000..f538f1f4 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt @@ -0,0 +1,37 @@ +package cn.coostack.cooparticlesapi.network.buffer + + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class FloatControlerBuffer() : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "float" + ) + ) + } + + override var loadedValue: Float? = 0.0f + override fun encode(value: Float): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Float { + return String(buf).toFloat() + + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt new file mode 100644 index 00000000..72236692 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt @@ -0,0 +1,45 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + +class IntArrayControlerBuffer : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "int_array" + ) + ) + } + + override var loadedValue: IntArray? = IntArray(0) + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun encode(value: IntArray): ByteArray { + val buffer = Unpooled.buffer() + buffer.writeInt(value.size) + value.forEach { buffer.writeInt(it) } + return buffer.copy().array() + } + + override fun decode(buf: ByteArray): IntArray { + val wrap = Unpooled.wrappedBuffer(buf) + val size = wrap.readInt() + val arr = IntArray(size) + for (i in 0 until size) { + arr[i] = wrap.readInt() + } + return arr + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt new file mode 100644 index 00000000..d2e89cf3 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt @@ -0,0 +1,35 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class IntControlerBuffer : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "int" + ) + ) + } + + override var loadedValue: Int? = 0 + override fun encode(value: Int): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Int { + return String(buf).toInt() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt new file mode 100644 index 00000000..fcf69566 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + +class LongArrayControlerBuffer : ParticleControlerDataBuffer { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "long_array" + ) + ) + } + + override var loadedValue: LongArray? = LongArray(0) + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun encode(value: LongArray): ByteArray { + val buffer = Unpooled.buffer() + buffer.writeInt(value.size) + value.forEach { + buffer.writeLong(it) + } + return buffer.copy().array() + } + + override fun decode(buf: ByteArray): LongArray { + val wrap = Unpooled.wrappedBuffer(buf) + val size = wrap.readInt() + val arr = LongArray(size) + for (i in 0 until size) { + arr[i] = wrap.readLong() + } + return arr + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt new file mode 100644 index 00000000..c968c112 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + + +class LongControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "long" + ) + ) + } + + override var loadedValue: Long? = 0L + override fun encode(value: Long): ByteArray { + return value.toString().toByteArray() + } + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Long { + return String(buf).toLong() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt new file mode 100644 index 00000000..4f40c30b --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt @@ -0,0 +1,71 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + + +/** + * 如果某个style使用了嵌套 (内容使用了其他Style或者group) + * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer + * 如果你使用了自定义的ParticleControlableDataBuffer + * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe + */ +class NestedBuffersControlerBuffer : ParticleControlerDataBuffer>> { + + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "nested" + ) + ) + } + + override var loadedValue: Map>? = null + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun encode(value: Map>): ByteArray { + val buf = Unpooled.buffer(value.size * 8) + // 第一个存 接下来的参数个数 + // 第一个 int代表key的长度 + // 第二个 int代表value类型的字符串类的长度 + // 第三个 int代表value的长度 + buf.writeInt(value.size) + value.forEach { (key, value) -> + buf.writeInt(key.length) + buf.writeBytes(key.toByteArray()) + val id = "${value.getBufferID().value.namespace}:${value.getBufferID().value.path}" + buf.writeInt(id.length) + buf.writeBytes(id.toByteArray()) + val subBytes = value.encode()!! + buf.writeInt(subBytes.size) + buf.writeBytes(subBytes) + } + return buf.copy().array() + } + + override fun decode(buf: ByteArray): Map> { + val buf = Unpooled.copiedBuffer(buf) + val len = buf.readInt() + val res = mutableMapOf>() + for (i in 0 until len) { + val keyLen = buf.readInt() + val key = String(buf.readBytes(keyLen).copy().array()) + val typeLen = buf.readInt() + val idSplit = String(buf.readBytes(typeLen).copy().array()).split(":") + val id = ResourceLocation.fromNamespaceAndPath(idSplit[0], idSplit[1]) + val valueLen = buf.readInt() + val valueBytes = buf.readBytes(valueLen).copy().array() + val buffer = ParticleControlerDataBuffers.withIdDecode(id, valueBytes)!! + res[key] = buffer + } + return res + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt new file mode 100644 index 00000000..b18ca163 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt @@ -0,0 +1,32 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import net.minecraft.resources.ResourceLocation + + +interface ParticleControlerDataBuffer { + + data class Id(val value: ResourceLocation) { + companion object { + @JvmStatic + fun toID(string: String): Id { + val split = string.split(":") + if (split.size != 2) { + throw IllegalArgumentException("Invalid ID format: $string") + } + val namespace = split[0] + val id = split[1] + return Id(ResourceLocation.fromNamespaceAndPath(namespace, id)) + } + } + } + + var loadedValue: T? + fun encode(): ByteArray? + + fun encode(value: T): ByteArray + + fun decode(buf: ByteArray): T + + fun getBufferID(): Id + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt new file mode 100644 index 00000000..acdc1c72 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt @@ -0,0 +1,184 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +object ParticleControlerDataBuffers { + private val wrapperToPrimitive = mapOf, Class<*>>( + java.lang.Integer::class.java to Int::class.java, + java.lang.Double::class.java to Double::class.java, + java.lang.Long::class.java to Long::class.java, + java.lang.Float::class.java to Float::class.java, + java.lang.Boolean::class.java to Boolean::class.java, + java.lang.Character::class.java to Char::class.java, + java.lang.Byte::class.java to Byte::class.java, + java.lang.Short::class.java to Short::class.java + ) + val registerBuilder = HashMap>>() + val registerTypes = HashMap, ParticleControlerDataBuffer.Id>() + + fun boolean(value: Boolean): BooleanControlerBuffer = BooleanControlerBuffer().apply { loadedValue = (value) } + fun char(value: Char): CharControlerBuffer = CharControlerBuffer().apply { loadedValue = (value) } + fun string(value: String): StringControlerBuffer = StringControlerBuffer().apply { loadedValue = (value) } + fun int(value: Int): IntControlerBuffer = IntControlerBuffer().apply { loadedValue = (value) } + fun intArray(value: IntArray): IntArrayControlerBuffer = IntArrayControlerBuffer().apply { loadedValue = (value) } + fun double(value: Double): DoubleControlerBuffer = DoubleControlerBuffer().apply { loadedValue = (value) } + fun float(value: Float): FloatControlerBuffer = FloatControlerBuffer().apply { loadedValue = (value) } + fun long(value: Long): LongControlerBuffer = LongControlerBuffer().apply { loadedValue = (value) } + fun longArray(value: LongArray): LongArrayControlerBuffer = + LongArrayControlerBuffer().apply { loadedValue = (value) } + + fun short(value: Short): ShortControlerBuffer = ShortControlerBuffer().apply { loadedValue = (value) } + fun vec3d(value: Vec3): Vec3dControlerBuffer = Vec3dControlerBuffer().apply { loadedValue = (value) } + fun relative(value: RelativeLocation): RelativeLocationControlerBuffer = + RelativeLocationControlerBuffer().apply { loadedValue = (value) } + + fun uuid(value: UUID): UUIDControlerBuffer = UUIDControlerBuffer().apply { loadedValue = (value) } + fun empty(): EmptyControlerBuffer = EmptyControlerBuffer() + + /** + * 如果某个style使用了嵌套 (内容使用了其他Style或者group) + * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer + * 如果你使用了自定义的ParticleControlableDataBuffer + * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe + */ + fun nested(value: Map>): NestedBuffersControlerBuffer = + NestedBuffersControlerBuffer().apply { loadedValue = value } + + fun withType(value: Any, clazz: Class>): ParticleControlerDataBuffer<*> { + val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + bufferCodec.loadedValue = value + return bufferCodec + } + + fun fromBufferType(value: Any, clazz: Class<*>): ParticleControlerDataBuffer<*>? { + val targetClass = registerTypes[clazz]?.let { clazz } + ?: wrapperToPrimitive[clazz]?.takeIf { registerTypes.containsKey(it) } + ?: return null + + return registerTypes[targetClass]?.let { withId(it, value) } + } + + fun withDecode(buf: ByteArray, clazz: Class>): ParticleControlerDataBuffer<*> { + val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + bufferCodec.loadedValue = bufferCodec.decode(buf) + return bufferCodec + } + + + fun withId(id: ParticleControlerDataBuffer.Id, value: Any): ParticleControlerDataBuffer<*>? { + val clazz = registerBuilder[id] ?: return null + return withType(value, clazz) + } + + fun withIdDecode(id: ParticleControlerDataBuffer.Id, array: ByteArray): ParticleControlerDataBuffer<*>? { + val clazz = registerBuilder[id] ?: return null + return withDecode(array, clazz) + } + + fun withId(id: ResourceLocation, value: Any): ParticleControlerDataBuffer<*>? { + val identifier = ParticleControlerDataBuffer.Id(id) + val clazz = registerBuilder[identifier] ?: return null + return withType(value, clazz) + } + + fun withIdDecode(id: ResourceLocation, array: ByteArray): ParticleControlerDataBuffer<*>? { + val identifier = ParticleControlerDataBuffer.Id(id) + val clazz = registerBuilder[identifier] ?: return null + return withDecode(array, clazz) + } + + + fun register( + bufType: Class<*>, + id: ParticleControlerDataBuffer.Id, + type: Class> + ) { + registerBuilder[id] = type + registerTypes[bufType] = id + } + + fun encode(buffer: ParticleControlerDataBuffer): ByteArray { + val code = buffer.encode() ?: throw IllegalStateException("buffer encode value is null") + val decoderID = buffer::class.java.name + val byteBuf = Unpooled.buffer() + byteBuf.writeInt(decoderID.length) + byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) + byteBuf.writeBytes(code) + return byteBuf.copy().array() + } + + fun encode(value: T, buffer: ParticleControlerDataBuffer): ByteArray { + val code = buffer.encode(value) + val decoderID = buffer::class.java.name + val byteBuf = Unpooled.buffer() + byteBuf.writeInt(decoderID.length) + byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) + byteBuf.writeBytes(code) + return byteBuf.copy().array() + } + + fun decode(bytes: ByteArray): T { + // 要求目标编码器必须具有空构造函数 + val byteBuf = Unpooled.wrappedBuffer(bytes) + val len = byteBuf.readInt() + val toStringBytes = ByteArray(len) + byteBuf.readBytes(toStringBytes) + val decoderID = String(toStringBytes, Charsets.UTF_8) + val code = byteBuf.readBytes(byteBuf.readableBytes()) + val codeBytes = ByteArray(code.readableBytes()) + code.readBytes(codeBytes) + val clazz = Class.forName(decoderID) + val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + + return ins.decode(codeBytes) + } + + fun decodeToBuffer(bytes: ByteArray): ParticleControlerDataBuffer { + val buf = Unpooled.wrappedBuffer(bytes) + return decodeToBuffer(buf) + } + + fun decodeToBuffer(byteBuf: ByteBuf): ParticleControlerDataBuffer { + // 要求目标编码器必须具有空构造函数 + val len = byteBuf.readInt() + val toStringBytes = ByteArray(len) + byteBuf.readBytes(toStringBytes) + val decoderID = String(toStringBytes, Charsets.UTF_8) + val code = byteBuf.readBytes(byteBuf.readableBytes()) + val codeBytes = ByteArray(code.readableBytes()) + code.readBytes(codeBytes) + val clazz = Class.forName(decoderID) + val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer + + return ins.apply { loadedValue = (ins.decode(codeBytes)) } + } + + + init { + register(Boolean::class.java, BooleanControlerBuffer.id, BooleanControlerBuffer::class.java) + register(Long::class.java, LongControlerBuffer.id, LongControlerBuffer::class.java) + register(Integer::class.java, IntControlerBuffer.id, IntControlerBuffer::class.java) + register(Double::class.java, DoubleControlerBuffer.id, DoubleControlerBuffer::class.java) + register(Float::class.java, FloatControlerBuffer.id, FloatControlerBuffer::class.java) + register(String::class.java, StringControlerBuffer.id, StringControlerBuffer::class.java) + register(IntArray::class.java, IntArrayControlerBuffer.id, IntArrayControlerBuffer::class.java) + register(LongArray::class.java, LongArrayControlerBuffer.id, LongArrayControlerBuffer::class.java) + register(UUID::class.java, UUIDControlerBuffer.id, UUIDControlerBuffer::class.java) + register(Vec3::class.java, Vec3dControlerBuffer.id, Vec3dControlerBuffer::class.java) + register( + RelativeLocation::class.java, + RelativeLocationControlerBuffer.id, + RelativeLocationControlerBuffer::class.java + ) + register(Short::class.java, ShortControlerBuffer.id, ShortControlerBuffer::class.java) + register(Unit::class.java, EmptyControlerBuffer.id, EmptyControlerBuffer::class.java) + register(Char::class.java, CharControlerBuffer.id, CharControlerBuffer::class.java) + register(Map::class.java, NestedBuffersControlerBuffer.id, NestedBuffersControlerBuffer::class.java) + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt new file mode 100644 index 00000000..0e4ad1f6 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt @@ -0,0 +1,36 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation + +class RelativeLocationControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "relative" + ) + ) + } + + override var loadedValue: RelativeLocation? = RelativeLocation() + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): RelativeLocation { + return RelativeLocation.fromBytes(buf) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + + override fun encode(value: RelativeLocation): ByteArray { + return value.toBytes() + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt new file mode 100644 index 00000000..cb9f978e --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class ShortControlerBuffer() : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "short" + ) + ) + } + override var loadedValue: Short? = 0 + override fun encode(value: Short): ByteArray { + return value.toString().toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Short { + return String(buf).toShort() + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt new file mode 100644 index 00000000..70e63b35 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation + +class StringControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "string" + ) + ) + } + override var loadedValue: String? = "" + override fun encode(value: String): ByteArray { + return value.toByteArray() + } + + + override fun encode(): ByteArray? { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): String { + return String(buf) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt new file mode 100644 index 00000000..2df22bab --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class UUIDControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "uuid" + ) + ) + } + override var loadedValue: UUID? = UUID.randomUUID() + override fun encode(value: UUID): ByteArray { + return value.toString().toByteArray() + } + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): UUID { + return UUID.fromString(String(buf)) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt new file mode 100644 index 00000000..8ff7da90 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt @@ -0,0 +1,41 @@ +package cn.coostack.cooparticlesapi.network.buffer + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import io.netty.buffer.Unpooled +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +class Vec3dControlerBuffer : ParticleControlerDataBuffer { + companion object { + @JvmStatic + val id = ParticleControlerDataBuffer.Id( + ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, "vec3d" + ) + ) + } + + override var loadedValue: Vec3? = Vec3.ZERO + + override fun encode(): ByteArray { + return encode(loadedValue!!) + } + + override fun decode(buf: ByteArray): Vec3 { + val wrappedBuffer = Unpooled.wrappedBuffer(buf) + return Vec3(wrappedBuffer.readDouble(), wrappedBuffer.readDouble(), wrappedBuffer.readDouble()) + } + + override fun getBufferID(): ParticleControlerDataBuffer.Id { + return id + } + + override fun encode(value: Vec3): ByteArray { + val buf = Unpooled.buffer() + buf.writeDouble(value.x) + buf.writeDouble(value.y) + buf.writeDouble(value.z) + return buf.copy().array() + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt new file mode 100644 index 00000000..4113c61f --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import net.minecraft.client.Minecraft + +/** + * 客户端收到 [CooPacket] 时的上下文 + * + * @property packet 已解码的业务包 + * @property kind 包类型 (普通/请求/响应) + * @property correlationId 关联ID, 0 表示普通包 + * @property timeoutTicks 请求模式下的超时tick (来自发送端) + */ +class ClientContext( + val packet: CooPacket, + val kind: CooPacketKind, + val correlationId: Long, + val timeoutTicks: Int, +) { + val client: Minecraft = Minecraft.getInstance() + + /** + * 在请求模式下,向服务端回复一个响应包 + * + * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 + * - 使用本上下文携带的 [correlationId],服务端 request 注册的回调将匹配执行 + */ + fun reply(response: CooPacket) { + if (kind != CooPacketKind.REQUEST) { + throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") + } + CooClientPacketManager.replyInternal(response, correlationId) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt new file mode 100644 index 00000000..12f93058 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt @@ -0,0 +1,250 @@ +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 + +/** + * # 客户端 CooPacket 管理器 + * + * 提供: + * - [sendTo] 发送普通包到服务器 + * - [request] 向服务器发起请求, 等待指定类型响应或超时 + * - [cancelRequest] 在响应/超时前取消等待 + * + * 同时负责 envelope 包的解码、回调分发、超时扫描 (由 [tick] 推进) + * + * 默认超时为 [DEFAULT_TIMEOUT_TICKS] (60 tick = 3 秒) + */ +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() + + /** + * 发送一个普通包到服务器 + * + * @param packet 待发送的业务包 + * @return 是否发送成功 (false = 事件被取消 或 包未注册) + */ + @JvmStatic + fun sendTo(packet: CooPacket): Boolean { + return sendInternal(packet, CooPacketKind.NORMAL, 0L, 0) + } + + /** + * 向服务器发起一个请求, 等待 [R] 类型的响应 + * + * @param packet 请求包 + * @param timeoutTicks 超时 tick 数, 默认 [DEFAULT_TIMEOUT_TICKS] + * @param responseType 响应包类型 (用于运行时校验, kotlin 里可用 reified 重载) + * @param onResponse 收到响应包时的回调 (主线程) + * @return 关联ID, 可用 [cancelRequest] 取消; 若发送失败返回 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 + } + + /** + * Kotlin 友好的 reified 版本 + */ + @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 + } + + /** + * 客户端 tick 推进, 由 CooParticlesAPIClient 主循环调用 + */ + @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, + ) + ) + } + } + + /** + * 处理接收自服务端的信封 (客户端线程) + * + * 由平台 (Fabric / NeoForge) 在收到 [CooPacketEnvelopeS2C] 时调用 + */ + @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("收到未知 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 异常: ${envelope.packetId}", e) + } + + if (kind == CooPacketKind.RESPONSE) { + val pendingEntry = pending.remove(envelope.correlationId) ?: return + if (!pendingEntry.expectType.isInstance(packet)) { + CooParticlesConstants.logger.warn( + "CooPacket 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" + ) + return + } + try { + pendingEntry.callback(packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket request 回调异常 (correlationId=${envelope.correlationId})", + e + ) + } + } + } + + /** + * 内部 reply: 由 [ClientContext.reply] 调用,构造响应包发送给服务端 + */ + 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 未注册, 无法发送: ${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 编码失败: ${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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt new file mode 100644 index 00000000..5aa97286 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt @@ -0,0 +1,56 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistryHelper +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation + +/** + * # 设计初衷 + * - 简化繁琐的数据包注册流程 采用 [cn.coostack.cooparticlesapi.annotations.CooAutoRegister] 快速注册 + * - 非必须定义包处理器 采用事件/回调方案 (基于 CooEvent) + * - 支持请求/回报的快捷方案 (基于 [CooServerPacketManager.request] / [CooClientPacketManager.request]) + * + * # 使用注意 + * - 必须提供空构造参数 (因为 codec 反射构造) + * - 所有的 CooPacket 类必须使用 [cn.coostack.cooparticlesapi.annotations.CodecField] 标明数据类型 + * - 必须使用 [cn.coostack.cooparticlesapi.annotations.CooAutoRegister] 进行自动注册标识 + * + * # 默认 timeout + * - 请求/响应模式默认超时为 60 tick (3秒) + * + * # 接收回调 + * - [onClientReceive] / [onServerReceive] 默认空实现 + * - 推荐通过 CooEvent 监听 [cn.coostack.cooparticlesapi.event.events.packet.CooPacketReceiveEvent] + * 或在子类内 override 上述方法 + */ +abstract class CooPacket { + /** + * 数据包ID 必须由子类提供,用于跨端定位包类型 + */ + abstract fun id(): ResourceLocation + + /** + * 数据包编解码器,默认基于反射 + [cn.coostack.cooparticlesapi.annotations.CodecField] 自动生成 + */ + open fun codec(): StreamCodec = + CooPacketRegistryHelper.generateClassParticleCodec(this::class.java) + + /** + * 收到自服务器的包时回调 (客户端线程) + * + * - 普通包: 直接回调 + * - 请求包: 子类应在此处理请求并通过 [ClientContext.reply] 回复 + * - 响应包: 也会调用本方法 (与 request 注册的回调并存) + */ + open fun onClientReceive(context: ClientContext) {} + + /** + * 收到自客户端的包时回调 (服务器线程) + * + * - 普通包: 直接回调 + * - 请求包: 子类应在此处理请求并通过 [ServerContext.reply] 回复 + * - 响应包: 也会调用本方法 (与 request 注册的回调并存) + */ + open fun onServerReceive(context: ServerContext) {} +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt new file mode 100644 index 00000000..63a52437 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +/** + * 数据包类型 + * + * - [NORMAL]: 普通包,无需响应 + * - [REQUEST]: 请求包,等待对端响应 + * - [RESPONSE]: 响应包,对应某个 REQUEST 的回执 + */ +enum class CooPacketKind { + NORMAL, + REQUEST, + RESPONSE; + + companion object { + private val values = entries.toTypedArray() + + fun fromId(id: Int): CooPacketKind { + return values.getOrNull(id) ?: NORMAL + } + } + + val id: Int get() = ordinal +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt new file mode 100644 index 00000000..e03b4951 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt @@ -0,0 +1,349 @@ +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.network.protocol.common.custom.CustomPacketPayload +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 + +/** + * # 服务端 CooPacket 管理器 + * + * 提供: + * - [sendTo] / [sendAll] / [sendWorlds] 普通包发送 + * - [request] / [requestAll] / [requestWorlds] 请求/响应 + * - [cancelRequest] 取消等待中的请求 + * + * 默认超时 [DEFAULT_TIMEOUT_TICKS] (60 tick = 3 秒). + * + * 同时负责 envelope 解码、回调分发、超时扫描 (由 [tick] 推进) + */ +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) + } + + /** + * 广播给所有在线玩家. 每个玩家独立触发 [CooPacketSendEvent], 可分别取消 + */ + @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) + } + + // ---------------- 请求 ---------------- + + /** + * 向指定玩家发起请求, 等待 [R] 类型响应 + * + * @return 关联ID; 0 表示发送失败 + */ + @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) + + /** + * 内部使用: 把 sender 也传给回调 (用于 requestAll / requestWorlds) + */ + @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 + } + + /** + * 给所有玩家分别发起独立请求 (correlationId 各自一份, 各自计时) + * + * @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, packet, R::class.java, timeoutTicks, onResponse) + + @JvmStatic + fun cancelRequest(correlationId: Long): Boolean { + return pending.remove(correlationId) != null + } + + /** + * 服务端 tick 推进, 由 CooParticlesAPI.tickServer 调用 + */ + @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, + ) + ) + } + } + + // ---------------- 接收 ---------------- + + /** + * 由平台层 (Fabric / NeoForge) 在收到 [CooPacketEnvelopeC2S] 时调用 + */ + @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( + "收到未知 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 异常: ${envelope.packetId}", e) + } + + if (kind == CooPacketKind.RESPONSE) { + val pendingEntry = pending.remove(envelope.correlationId) ?: return + if (!pendingEntry.expectType.isInstance(packet)) { + CooParticlesConstants.logger.warn( + "CooPacket 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" + ) + return + } + try { + pendingEntry.callback(sender, packet) + } catch (e: Throwable) { + CooParticlesConstants.logger.error( + "CooPacket request 回调异常 (correlationId=${envelope.correlationId})", + e + ) + } + } + } + + // ---------------- reply ---------------- + + 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 未注册, 无法发送: ${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 编码失败: ${packet::class.java.name}", e) + return false + } + val envelope: CustomPacketPayload = 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt new file mode 100644 index 00000000..5d1dd997 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt @@ -0,0 +1,33 @@ +package cn.coostack.cooparticlesapi.network.packet.api + +import net.minecraft.server.level.ServerPlayer + +/** + * 服务端收到 [CooPacket] 时的上下文 + * + * @property sender 发送该包的玩家 + * @property packet 已解码的业务包 + * @property kind 包类型 (普通/请求/响应) + * @property correlationId 关联ID, 0 表示普通包 + * @property timeoutTicks 请求模式下的超时tick (来自发送端) + */ +class ServerContext( + val sender: ServerPlayer, + val packet: CooPacket, + val kind: CooPacketKind, + val correlationId: Long, + val timeoutTicks: Int, +) { + /** + * 在请求模式下,向客户端回复一个响应包 + * + * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 + * - 使用本上下文携带的 [correlationId],客户端 request 注册的回调将匹配执行 + */ + fun reply(response: CooPacket) { + if (kind != CooPacketKind.REQUEST) { + throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") + } + CooServerPacketManager.replyInternal(sender, response, correlationId) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt new file mode 100644 index 00000000..ab9f4be1 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt @@ -0,0 +1,52 @@ +package cn.coostack.cooparticlesapi.network.packet.api.envelope + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +/** + * # CooPacket 信封 (C2S) + * + * 与 [CooPacketEnvelopeS2C] 字段一致,仅 payloadID 不同以匹配 MC 注册要求。 + */ +class CooPacketEnvelopeC2S( + val kindId: Int, + val packetId: ResourceLocation, + val correlationId: Long, + val timeoutTicks: Int, + val data: ByteArray, +) : CustomPacketPayload { + + companion object { + private val identifier = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "coo_packet_envelope_c2s") + + @JvmField + val payloadID: CustomPacketPayload.Type = + CustomPacketPayload.Type(identifier) + + @JvmField + val CODEC: StreamCodec = CustomPacketPayload.codec( + { packet, buf -> + buf.writeVarInt(packet.kindId) + buf.writeResourceLocation(packet.packetId) + buf.writeLong(packet.correlationId) + buf.writeVarInt(packet.timeoutTicks) + buf.writeByteArray(packet.data) + }, + { buf -> + CooPacketEnvelopeC2S( + kindId = buf.readVarInt(), + packetId = buf.readResourceLocation(), + correlationId = buf.readLong(), + timeoutTicks = buf.readVarInt(), + data = buf.readByteArray() + ) + } + ) + } + + override fun type(): CustomPacketPayload.Type = payloadID +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt new file mode 100644 index 00000000..ab5c12b2 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt @@ -0,0 +1,58 @@ +package cn.coostack.cooparticlesapi.network.packet.api.envelope + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +/** + * # CooPacket 信封 + * + * 所有[cn.coostack.cooparticlesapi.network.packet.api.CooPacket] 都通过本信封发送/接收 + * + * - kindId: 0=NORMAL, 1=REQUEST, 2=RESPONSE + * - packetId: 业务包的 ResourceLocation, 由 CooPacket.id() 提供 + * - correlationId: 0 表示普通包;非0 表示请求/响应关联ID + * - timeoutTicks: 仅请求时有效 + * - data: 业务包字段编码后的字节流 + */ +class CooPacketEnvelopeS2C( + val kindId: Int, + val packetId: ResourceLocation, + val correlationId: Long, + val timeoutTicks: Int, + val data: ByteArray, +) : CustomPacketPayload { + + companion object { + private val identifier = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "coo_packet_envelope_s2c") + + @JvmField + val payloadID: CustomPacketPayload.Type = + CustomPacketPayload.Type(identifier) + + @JvmField + val CODEC: StreamCodec = CustomPacketPayload.codec( + { packet, buf -> + buf.writeVarInt(packet.kindId) + buf.writeResourceLocation(packet.packetId) + buf.writeLong(packet.correlationId) + buf.writeVarInt(packet.timeoutTicks) + buf.writeByteArray(packet.data) + }, + { buf -> + CooPacketEnvelopeS2C( + kindId = buf.readVarInt(), + packetId = buf.readResourceLocation(), + correlationId = buf.readLong(), + timeoutTicks = buf.readVarInt(), + data = buf.readByteArray() + ) + } + ) + } + + override fun type(): CustomPacketPayload.Type = payloadID +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt new file mode 100644 index 00000000..72827e45 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt @@ -0,0 +1,56 @@ +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.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +/** + * 客户端按键动作触发时发送给服务器 + */ +class PacketKeyActionC2S( + val keyActions: KeyActionBatch +) : CustomPacketPayload { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_action") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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)) + }) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt new file mode 100644 index 00000000..7d545a33 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt @@ -0,0 +1,80 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketCameraShakeS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.utils.ClientCameraUtil +import net.minecraft.world.phys.Vec3 + +object ClientCameraShakeHandler { + fun receive( + payload: PacketCameraShakeS2C, + context: ClientContext + ) { + when (payload.operation) { + PacketCameraShakeS2C.CameraOperation.SHAKE -> handleShake(payload, context) + PacketCameraShakeS2C.CameraOperation.SET_OFFSET -> { + if (payload.instant) { + ClientCameraUtil.setOffsetNow(payload.position, payload.yawOffset, payload.pitchOffset) + } else { + ClientCameraUtil.setOffset(payload.position, payload.yawOffset, payload.pitchOffset) + } + } + + PacketCameraShakeS2C.CameraOperation.RESET_OFFSET -> { + if (payload.instant) { + ClientCameraUtil.resetOffsetNow() + } else { + ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) + } + } + + PacketCameraShakeS2C.CameraOperation.FORCE_POSITION -> { + if (payload.instant) { + ClientCameraUtil.setForcedCameraPositionNow(payload.position) + } else { + ClientCameraUtil.setForcedCameraPosition(payload.position) + } + } + + PacketCameraShakeS2C.CameraOperation.RESET_FORCE_POSITION -> { + if (payload.instant) { + ClientCameraUtil.resetForcedCameraPositionNow() + } else { + ClientCameraUtil.resetForcedCameraPosition() + } + } + + PacketCameraShakeS2C.CameraOperation.RESET_ALL -> { + if (payload.instant) { + ClientCameraUtil.resetAllNow() + } else { + ClientCameraUtil.stopShakeCamera() + ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) + ClientCameraUtil.resetForcedCameraPosition() + } + } + } + } + + private fun handleShake(payload: PacketCameraShakeS2C, context: ClientContext) { + val range = payload.range + val player = context.player() + var amplitude = payload.amplitude + var frequency = payload.frequency + if (range > 0) { + val distance = player.position().distanceTo(payload.origin) + if (distance > range) { + return + } + if (payload.attenuateByDistance) { + val attenuation = (1.0 - distance / range).coerceIn(0.0, 1.0) + amplitude *= attenuation + frequency *= attenuation + } + } + if (amplitude <= 0.0 || frequency <= 0.0) { + return + } + ClientCameraUtil.startShakeCamera(payload.tick, amplitude, frequency) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt new file mode 100644 index 00000000..f0a997e6 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesAPIClient +import cn.coostack.cooparticlesapi.network.packet.server.PacketClearClientStateS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientClearStateHandler { + fun receive(payload: PacketClearClientStateS2C, context: ClientContext) { + context.client().execute { + CooParticlesAPIClient.clearTransientClientState() + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt new file mode 100644 index 00000000..6e803e15 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.data.holder.DataHolderManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketDataHolderS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientDataHolderPacketHandler { + fun receive( + payload: PacketDataHolderS2C, + context: ClientContext + ) { + val entity = context.player().level().getEntity(payload.entityId) ?: return + DataHolderManager.applyClient(entity, payload) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt new file mode 100644 index 00000000..ec9f5d7e --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt @@ -0,0 +1,50 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.display.DisplayEntity +import cn.coostack.cooparticlesapi.display.DisplayEntityManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.RegistryFriendlyByteBuf + +object ClientDisplayEntityPacketHandler { + fun receive( + payload: PacketDisplayEntityS2C, + context: ClientContext + ) { + context.client().execute { + apply(payload, context) + } + } + + private fun apply(payload: PacketDisplayEntityS2C, context: ClientContext) { + if (payload.removed) { + DisplayEntityManager.clientView.remove(payload.uuid) + return + } + val new = decodeData(payload) + new.world = context.player().level() + val old = DisplayEntityManager.clientView[payload.uuid] ?: let { + // 新建 + DisplayEntityManager.addClient(new) + return + } + // 更新 + old.update(new) + } + + private fun decodeData(payload: PacketDisplayEntityS2C): DisplayEntity { + val data = payload.data + val type = payload.type + val codec = DisplayEntityManager.registeredTypes[type]!! + val new = codec.decode( + RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(data), + Minecraft.getInstance().player!!.registryAccess() + ) + ) + return new + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt new file mode 100644 index 00000000..980fe036 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt @@ -0,0 +1,19 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.display.DisplayEntityManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityStateS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientDisplayEntityStateHandler { + fun receive(payload: PacketDisplayEntityStateS2C, context: ClientContext) { + context.client().execute { + DisplayEntityManager.clientView[payload.uuid]?.applyRemoteState( + payload.position, + payload.yaw, + payload.pitch, + payload.roll, + payload.scale, + ) + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt new file mode 100644 index 00000000..9c36c138 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt @@ -0,0 +1,15 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.key.CooKeyBindingManager +import cn.coostack.cooparticlesapi.network.packet.server.PacketKeyBindingCountdownS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientKeyBindingCountdownHandler { + fun receive( + payload: PacketKeyBindingCountdownS2C, + context: ClientContext + ) { + CooKeyBindingManager.setCountdown(payload.key, payload.cd) + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt new file mode 100644 index 00000000..b8d7efcd --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt @@ -0,0 +1,89 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionS2C +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.RegistryFriendlyByteBuf +import java.util.concurrent.ConcurrentHashMap + +object ClientParticleCompositionHandler { + private val missingCodecWarnings = ConcurrentHashMap.newKeySet() + private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() + + fun receive( + payload: PacketParticleCompositionS2C, + context: ClientContext + ) { + context.client().execute { + apply(payload, context) + } + } + + private fun apply(payload: PacketParticleCompositionS2C, context: ClientContext) { + val distanceRemove = payload.distanceRemove + // 优化新建 + var new: ParticleComposition? = null + val old = ParticleCompositionManager.clientView[payload.uuid] + if (!distanceRemove) { + // 新建 如果old为null 则直接作为新的添加在里面 + new = decodeData(payload) ?: return + if (new.controlUUID != payload.uuid && identityMismatchWarnings.add(payload.type)) { + CooParticlesConstants.logger.warn( + "Composition 网络 UUID 不一致,已采用数据包 UUID: type={}, packet={}, decoded={}", + payload.type, + payload.uuid, + new.controlUUID, + ) + } + new.controlUUID = payload.uuid + new.world = context.player().level() + } + if (old == null && !distanceRemove) { + ParticleCompositionManager.addClient(new!!) + // fix 当composition消散的时候 会抛出npe的问题 + return + } + val current = old ?: return + // new == null时 distanceRemove应该为true + if (distanceRemove) { + current.remove() + } else { + if (payload.recreate || current.canceled || !current.displayed) { + current.clear(true) + ParticleCompositionManager.addClient(new!!) + return + } + // new 不是null 且 old 不为null 更新old + current.update(new!!) + } + } + + private fun decodeData(payload: PacketParticleCompositionS2C): ParticleComposition? { + val data = payload.data + val type = payload.type + val codec = ParticleCompositionManager.registeredTypes[type] ?: run { + if (missingCodecWarnings.add(type)) { + CooParticlesConstants.logger.warn( + "收到未注册的 Composition codec: type={}, uuid={}", + type, + payload.uuid, + ) + } + return null + } + val buffer = RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(data), + Minecraft.getInstance().player!!.registryAccess() + ) + return try { + codec.decode(buffer) + } finally { + buffer.release() + } + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt new file mode 100644 index 00000000..ac7c7f34 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.extend.asRelative +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionRotateS2C +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientParticleCompositionRotateHandler { + fun receive( + payload: PacketParticleCompositionRotateS2C, + context: ClientContext + ) { + context.client().execute { + val composition = ParticleCompositionManager.clientView[payload.uuid] ?: return@execute + composition.applyRemoteRotation(payload.direction?.asRelative(), payload.rollDelta) + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt new file mode 100644 index 00000000..70b29589 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt @@ -0,0 +1,20 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionStateS2C +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientParticleCompositionStateHandler { + fun receive(payload: PacketParticleCompositionStateS2C, context: ClientContext) { + context.client().execute { + ParticleCompositionManager.clientView[payload.uuid]?.applyRemoteState( + payload.position, + payload.visibleRange, + payload.scale, + payload.displayStatus, + payload.closedInterval, + payload.current, + ) + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt new file mode 100644 index 00000000..eac2c286 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt @@ -0,0 +1,75 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleEmittersS2C +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmittersManager +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.RegistryFriendlyByteBuf +import java.util.concurrent.ConcurrentHashMap + +object ClientParticleEmittersPacketHandler { + private val missingCodecWarnings = ConcurrentHashMap.newKeySet() + private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() + + fun receive( + payload: PacketParticleEmittersS2C, + context: ClientContext + ) { + context.client().execute { + when (payload.type) { + PacketParticleEmittersS2C.PacketType.CREATE -> handleCreate(payload, context) + PacketParticleEmittersS2C.PacketType.CHANGE -> handleChange(payload, context) + PacketParticleEmittersS2C.PacketType.REMOVE -> handleRemove(payload) + } + } + } + + private fun handleCreate(payload: PacketParticleEmittersS2C, context: ClientContext) { + val emitters = decode(payload) ?: return + ParticleEmittersManager.createClient(emitters, context.player().level()) + } + + private fun handleChange(payload: PacketParticleEmittersS2C, context: ClientContext) { + val emitters = decode(payload) ?: return + ParticleEmittersManager.changeClient(emitters, context.player().level()) + } + + private fun handleRemove(payload: PacketParticleEmittersS2C) { + ParticleEmittersManager.removeClient(payload.emitterUUID) + } + + private fun decode(payload: PacketParticleEmittersS2C): ParticleEmitters? { + val codec = ParticleEmittersManager.getCodecFromID(payload.emitterID) ?: run { + if (missingCodecWarnings.add(payload.emitterID)) { + CooParticlesConstants.logger.warn( + "收到未注册的 Emitter codec: id={}, uuid={}", + payload.emitterID, + payload.emitterUUID, + ) + } + return null + } + val buffer = RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(payload.emitterData), + Minecraft.getInstance().level!!.registryAccess(), + ) + return try { + codec.decode(buffer).also { emitters -> + if (emitters.uuid != payload.emitterUUID && identityMismatchWarnings.add(payload.emitterID)) { + CooParticlesConstants.logger.warn( + "Emitter 网络 UUID 不一致,已采用数据包 UUID: id={}, packet={}, decoded={}", + payload.emitterID, + payload.emitterUUID, + emitters.uuid, + ) + } + emitters.uuid = payload.emitterUUID + } + } finally { + buffer.release() + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt new file mode 100644 index 00000000..62939ddd --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt @@ -0,0 +1,126 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.particles.control.group.ClientParticleGroupManager +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.particles.control.group.SequencedParticleGroup +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.Minecraft +import net.minecraft.world.phys.Vec3 +import java.util.UUID + + +object ClientParticleGroupPacketHandler { + fun receive(packet: PacketParticleGroupS2C, context: ClientContext) { + // Group UUID + val uuid = packet.uuid + // 修改类型 + val type = packet.type + when (type) { + ControlType.CREATE -> handleCreate(uuid, packet.args) + ControlType.CHANGE -> handleChange(uuid, packet.args) + ControlType.REMOVE -> handleRemove(uuid) + } + } + + + private fun handleCreate( + groupUUID: UUID, + args: Map> + ) { + val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]?.loadedValue!! as Vec3 + val world = Minecraft.getInstance().level!! + val type = args[PacketParticleGroupS2C.PacketArgsType.GROUP_TYPE.ofArgs]!!.loadedValue as String + val currentTick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue as Int + val maxTick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue as Int + val scale = args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue as Double + val groupClass = Class.forName(type) + val builder = ClientParticleGroupManager.getBuilder(groupClass as Class)!! + val group = builder.createGroup(groupUUID, args) + if (group is SequencedParticleGroup) { + handleSequencedGroupArgs(group, args) + } + group.tick = currentTick + group.maxTick = maxTick + group.scale(scale) + group.display(pos, world) + ClientParticleGroupManager.addVisibleGroup(group) + } + + private fun handleChange( + groupUUID: UUID, + args: Map> + ) { + val targetGroup = ClientParticleGroupManager.getControlGroup(groupUUID) ?: return + val argKeys = args.keys + if (PacketParticleGroupS2C.PacketArgsType.POS.ofArgs in argKeys) { + val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]!!.loadedValue!! as Vec3 + targetGroup.teleportGroupTo(pos) + } + if (PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs in argKeys) { + val to = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs]!!.loadedValue!! as Vec3 + targetGroup.rotateToPoint(RelativeLocation.of(to)) + } + if (PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs in argKeys) { + val angle = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs]!!.loadedValue as Double + targetGroup.rotateAsAxis(angle) + } + if (PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs in argKeys) { + val axis = args[PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs]!!.loadedValue!! as Vec3 + targetGroup.axis = RelativeLocation.of(axis) + } + if (PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs in argKeys) { + val tick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue!! as Int + targetGroup.tick = tick + } + if (PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs in argKeys) { + val tick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue!! as Int + targetGroup.maxTick = tick + } + if (PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs in argKeys) { + val targetMethodName = + args[PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs]!!.loadedValue!! as String + targetGroup::class.java.getDeclaredMethod(targetMethodName).apply { + isAccessible = true + }.invoke(targetGroup) + } + + if (PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs in argKeys) { + targetGroup.scale( + args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue!! as Double + ) + } + if (targetGroup is SequencedParticleGroup) { + handleSequencedGroupArgs(targetGroup, args) + } + + val builder = ClientParticleGroupManager.getBuilder(targetGroup::class.java) ?: return + builder.changeGroup(targetGroup, args) + } + + private fun handleRemove(groupUUID: UUID) { + ClientParticleGroupManager.removeVisible(groupUUID) + } + + private fun handleSequencedGroupArgs( + group: SequencedParticleGroup, + args: Map> + ) { + if (args.containsKey("toggle")) { + group.toggle(args["toggle"]!!.loadedValue as Int) + } + if (args.containsKey("addCount")) { + group.addMultiple(args["addCount"]!!.loadedValue as Int) + } + if (args.containsKey("removeCount")) { + group.removeMultiple(args["removeCount"]!!.loadedValue as Int) + } + + if (args.containsKey("toggle_status")) { + group.toggleStatus(args["toggle_status"]!!.loadedValue as LongArray) + } + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt new file mode 100644 index 00000000..e55ab4d1 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt @@ -0,0 +1,32 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext + +object ClientParticlePacketHandler { + fun receive(packet: PacketParticleBatchS2C, context: ClientContext) { + val world = context.player().level() + packet.positions.forEach { position -> + world.addParticle( + packet.type, + true, + position.x, + position.y, + position.z, + packet.velocity.x, + packet.velocity.y, + packet.velocity.z, + ) + } + } + + fun receive(packet: PacketParticleS2C, context: ClientContext) { + val player = context.player() + val world = player.level() + world.addParticle( + packet.type, true, packet.pos.x, packet.pos.y, packet.pos.z, + packet.velocity.x, packet.velocity.y, packet.velocity.z, + ) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt new file mode 100644 index 00000000..66bacfc1 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt @@ -0,0 +1,94 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C +import cn.coostack.cooparticlesapi.network.particle.style.ParticleGroupStyle +import cn.coostack.cooparticlesapi.network.particle.style.ParticleStyleManager +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.Minecraft +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +object ClientParticleStylePacketHandler { + fun receive(packet: PacketParticleStyleS2C, context: ClientContext) { + // Group UUID + val uuid = packet.uuid + // 修改类型 + val type = packet.type + when (type) { + ControlType.CREATE -> handleCreate(uuid, packet.args) + ControlType.CHANGE -> handleChange(uuid, packet.args) + ControlType.REMOVE -> handleRemove(uuid) + } + } + + private fun handleRemove(uuid: UUID) { + ParticleStyleManager.clientViewStyles[uuid]?.remove() + } + + /** 处理客户端同步修改 */ + private fun handleChange( + uuid: UUID, + args: Map> + ) { + // 确保他在 clientView + val style = ParticleStyleManager.clientViewStyles[uuid] ?: return + if (args.containsKey("pos")) { + val pos = args["pos"]!!.loadedValue as Vec3 + style.teleportTo(pos) + } + if (args.containsKey("teleport")) { + val pos = args["teleport"]!!.loadedValue as Vec3 + style.teleportTo(pos) + } + if (args.containsKey("rotate_to") && args.containsKey("rotate_angle")) { + style.rotateToWithAngle( + args["rotate_to"]!!.loadedValue as RelativeLocation, + args["rotate_angle"]!!.loadedValue as Double + ) + } else { + if (args.containsKey("rotate_angle")) { + style.rotateAsAxis(args["rotate_angle"]!!.loadedValue as Double) + } + if (args.containsKey("rotate_to")) { + style.rotateToPoint(args["rotate_to"]!!.loadedValue as RelativeLocation) + } + } + if (args.containsKey("axis")) { + style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) + } + if (args.containsKey("scale")) { + style.scale = args["scale"]!!.loadedValue as Double + } + if (args.containsKey("lastUpdatedGameTime")) { + style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long + } + if (args.containsKey("displayedTime")) { + style.displayedTime = args["displayedTime"]!!.loadedValue as Long + } + style.readPacketArgs(args) + } + + /** 处理客户端同步创建 */ + private fun handleCreate( + uuid: UUID, + args: Map> + ) { + val clazz = args["style_type"]!!.loadedValue as String + val builderType = Class.forName(clazz) + val builder = ParticleStyleManager.getBuilder(builderType as Class) ?: return + val style = builder.createStyle(uuid, args) + val pos = args["pos"]!!.loadedValue as Vec3 + style.rotate = args["rotate"]!!.loadedValue as Double + style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) + style.scale = args["scale"]!!.loadedValue as Double + style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long + style.displayedTime = args["displayedTime"]!!.loadedValue as Long + val world = Minecraft.getInstance().level + style.readPacketArgs(args) + ParticleStyleManager.spawnStyle(world!!, pos, style) + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt new file mode 100644 index 00000000..acacd8c5 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt @@ -0,0 +1,89 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.coofx.server.CooFxSceneRenderEntity +import cn.coostack.cooparticlesapi.network.packet.server.PacketRenderEntityS2C +import cn.coostack.cooparticlesapi.coofx.client.CooFxSceneClientRegistry +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.renderer.client.ClientRenderEntityManager +import cn.coostack.cooparticlesapi.renderer.runtime.ClientRenderEntityRegistry +import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityInstance +import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityRenderer +import cn.coostack.cooparticlesapi.renderer.RenderEntity +import io.netty.buffer.Unpooled +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.resources.ResourceLocation + +/** 把服务端 RenderEntity 同步包应用到客户端实体运行时。 */ +object ClientRenderEntityPacketHandler { + /** + * 解码同步数据,并在客户端线程执行创建、更新或移除操作。 + * + * @param packet 服务端发送的 RenderEntity 操作和序列化数据 + * @param context 提供客户端线程与当前客户端实例的网络上下文 + */ + fun receive( + packet: PacketRenderEntityS2C, + context: ClientContext + ) { + val method = packet.method + val data = packet.entityData + val id = packet.id + val buf = FriendlyByteBuf(Unpooled.wrappedBuffer(data)) + val type = ClientRenderEntityRegistry.get(id) + if (type == null) { + CooParticlesConstants.logger.error( + "[CooFX-CREATE] 客户端 RenderEntity 类型未注册:method=$method, packetUuid=${packet.uuid}, typeId=$id", + ) + return + } + val entity = type.codec.decode(buf) + context.client().execute { + entity.world = context.client().level + when (method) { + PacketRenderEntityS2C.Method.CREATE -> { + val renderer = resolveRenderer(entity, id) + val instance = RenderEntityInstance(entity, renderer) + ClientRenderEntityManager.add(instance) + (entity as? CooFxSceneRenderEntity)?.let { created -> + CooFxSceneClientRegistry.onCreated(created) + } + } + + PacketRenderEntityS2C.Method.TOGGLE -> { + val instance = ClientRenderEntityManager.getFrom(packet.uuid) + instance?.updateFrom(entity) + (instance?.entity as? CooFxSceneRenderEntity)?.let { updated -> + CooFxSceneClientRegistry.onUpdated(updated) + } + } + + PacketRenderEntityS2C.Method.REMOVE -> { + ClientRenderEntityManager.getFrom(packet.uuid)?.markRemoved() + CooFxSceneClientRegistry.onRemoved(packet.uuid) + } + } + } + } + + /** + * 解析实体使用的 renderer。旧式实体自带 renderer 时保留实例行为,V2 类型从注册表取共享实例。 + * + * @param entity 已解码的客户端实体 + * @param id 实体类型注册 id + * @return 可渲染当前实体的 renderer + * @throws IllegalStateException 类型没有注册 renderer 时抛出 + */ + @Suppress("UNCHECKED_CAST") + private fun resolveRenderer( + entity: RenderEntity, + id: ResourceLocation + ): RenderEntityRenderer { + if (entity is RenderEntityRenderer<*>) { + return entity as RenderEntityRenderer + } + val renderer = ClientRenderEntityRegistry.resolveRenderer(id) + ?: throw IllegalStateException("RenderEntity renderer not registered: $id") + return renderer as RenderEntityRenderer + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt new file mode 100644 index 00000000..b447f9e3 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt @@ -0,0 +1,41 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.packet.server.PacketRendererPostEffectS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.renderer.post.CooPostEffects +import cn.coostack.cooparticlesapi.renderer.post.PostEffectRuntimeRegistry + +object ClientRendererPostEffectHandler { + fun receive(packet: PacketRendererPostEffectS2C, context: ClientContext) { + context.client().execute { + apply(packet) + } + } + + internal fun apply(packet: PacketRendererPostEffectS2C) { + when (packet.operation) { + PacketRendererPostEffectS2C.Operation.CREATE, + PacketRendererPostEffectS2C.Operation.UPDATE -> applyState(packet) + PacketRendererPostEffectS2C.Operation.REMOVE -> CooPostEffects.client.remove(packet.instanceId) + } + } + + private fun applyState(packet: PacketRendererPostEffectS2C) { + val state = packet.state ?: return + if (!PostEffectRuntimeRegistry.containsType(state.effectType)) { + CooParticlesConstants.logger.warn( + "Skipping synced post effect id={} because type={} is not registered on the client", + state.instanceId, + state.effectType + ) + return + } + val instance = state.instantiate(serverSynced = true) ?: return + when (packet.operation) { + PacketRendererPostEffectS2C.Operation.CREATE -> CooPostEffects.client.add(instance) + PacketRendererPostEffectS2C.Operation.UPDATE -> CooPostEffects.client.update(instance) + PacketRendererPostEffectS2C.Operation.REMOVE -> Unit + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt new file mode 100644 index 00000000..34d09f43 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundInstanceS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.supports.sound.ClientSoundManager + +object ClientSoundInstanceHandler { + fun receive(payload: PacketSoundInstanceS2C, context: ClientContext) { + ClientSoundManager.handle(payload) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt new file mode 100644 index 00000000..613486cc --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt @@ -0,0 +1,11 @@ +package cn.coostack.cooparticlesapi.network.packet.client.listener + +import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundLoopS2C +import cn.coostack.cooparticlesapi.platform.network.ClientContext +import cn.coostack.cooparticlesapi.supports.sound.ClientSoundLoopManager + +object ClientSoundLoopHandler { + fun receive(payload: PacketSoundLoopS2C, context: ClientContext) { + ClientSoundLoopManager.handle(payload) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt new file mode 100644 index 00000000..32a3b9b2 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt @@ -0,0 +1,67 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.renderer.pipeline.CooBlockPipelines +import cn.coostack.cooparticlesapi.test.options.renderer.pipeline.RenderPipelineExamples +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.state.BlockState +import java.util.UUID + +/** + * 把方块测试选中的方块类型同步为客户端星空 FBO 临时绑定。 + * + * BlockState 只用于通过注册表同步方块类型。每次测试都有独立的 [bindingId],结束时只撤销 + * 自己的规则,之前的永久绑定或其他测试绑定会继续生效。 + */ +@CooAutoRegister +class PacketBindStarfieldFboBlockS2C() : CooPacket() { + /** 用于同步目标方块类型的状态载体。 */ + @CodecField + var state: BlockState = Blocks.AIR.defaultBlockState() + + /** 当前临时绑定的所有者 ID。 */ + @CodecField + var bindingId: UUID = UUID(0L, 0L) + + /** `true` 添加临时绑定,`false` 撤销该所有者的绑定。 */ + @CodecField + var enabled: Boolean = true + + /** + * @param state 测试位置的方块状态,用于传递方块类型 + * @param bindingId 当前测试的临时绑定 ID + * @param enabled 是否启用星空 FBO 绑定 + */ + constructor(state: BlockState, bindingId: UUID, enabled: Boolean) : this() { + this.state = state + this.bindingId = bindingId + this.enabled = enabled + } + + /** @return 星空 FBO 方块类型绑定包的稳定资源 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** + * @param context 当前客户端网络上下文 + */ + override fun onClientReceive(context: ClientContext) { + context.client.execute { + if (enabled) { + CooBlockPipelines.bindScoped(bindingId, state.block, RenderPipelineExamples.STARFIELD_BLOCK) + } else { + CooBlockPipelines.unbindScoped(bindingId) + } + } + } + + companion object { + private const val PACKET_ID = "bind_starfield_fbo_block_s2c" + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt new file mode 100644 index 00000000..4e283069 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt @@ -0,0 +1,210 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +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 +) : CustomPacketPayload { + 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 = CustomPacketPayload.Type(identifierID) + + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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.values().firstOrNull { it.id == id } ?: CameraOperation.SHAKE + } + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt new file mode 100644 index 00000000..001dae36 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt @@ -0,0 +1,20 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +object PacketClearClientStateS2C : CustomPacketPayload { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "clear_client_state") + + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ _, _ -> }, { _ -> PacketClearClientStateS2C }) + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt new file mode 100644 index 00000000..db295522 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +/** + * 旧版 BlockTest 的一次性 CooFX S2C 包已由服务端权威 `CooFxSceneManager` 和 RenderEntity + * CREATE/TOGGLE/REMOVE 协议替代。本文件保留迁移说明,避免再次引入测试专用网络状态。 + */ diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt new file mode 100644 index 00000000..c8d32fe5 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt @@ -0,0 +1,109 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.data.holder.DataHolder +import cn.coostack.cooparticlesapi.data.holder.DataHolderKey +import cn.coostack.cooparticlesapi.data.holder.DataHolderManager +import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.Entity + +class PacketDataHolderS2C( + val entityId: Int, + val cacheAllToggle: Boolean, + val fullSync: Boolean, + val entries: List +) : CustomPacketPayload { + 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 = CustomPacketPayload.Type(id) + val CODEC: StreamCodec = + StreamCodec.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: Entity, 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: Entity): ByteArray { + val codec = DataHolderManager.getCodecFromID(type) + ?: throw IllegalStateException("DataHolder codec not registered for type: $type") + val buf = RegistryFriendlyByteBuf( + Unpooled.buffer(), + entity.level().registryAccess() + ) + @Suppress("UNCHECKED_CAST") + (codec as StreamCodec).encode(buf, value) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + return data + } + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } + + fun decodeData(): Map, Any> { + val entity = Minecraft.getInstance().level?.getEntity(entityId) + ?: return emptyMap() + return entries.associate { entry -> + val keyType = runCatching { + Class.forName(entry.type) + }.getOrDefault(Any::class.java) + val key = DataHolderKey.ofRaw(keyType, ResourceLocation.parse(entry.key)) + key to decodeValue(entry.type, entry.data, entity) + } + } + + private fun decodeValue(type: String, data: ByteArray, entity: Entity): Any { + val codec = DataHolderManager.getCodecFromID(type) + ?: throw IllegalStateException("DataHolder codec not registered for type: $type") + val buf = RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(data), + Minecraft.getInstance().connection?.registryAccess() + ?: entity.level().registryAccess() + ) + @Suppress("UNCHECKED_CAST") + return (codec as StreamCodec).decode(buf) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt new file mode 100644 index 00000000..c3ab0b3a --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt @@ -0,0 +1,40 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import java.util.UUID + + +class PacketDisplayEntityS2C( + val uuid: UUID, + val type: String, + val data: ByteArray, + val removed: Boolean = false +) : CustomPacketPayload { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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) + }) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt new file mode 100644 index 00000000..bfd7d448 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt @@ -0,0 +1,44 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +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, +) : CustomPacketPayload { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity_state") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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(), + ) + }) + } + + override fun type(): CustomPacketPayload.Type = payloadID +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt new file mode 100644 index 00000000..d5a6442d --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt @@ -0,0 +1,26 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +class PacketKeyBindingCountdownS2C(val key: ResourceLocation, val cd: Int) : CustomPacketPayload { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_binding_countdown") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ packet, buf -> + buf.writeResourceLocation(packet.key) + buf.writeInt(packet.cd) + }, { buf -> + PacketKeyBindingCountdownS2C(buf.readResourceLocation(), buf.readInt()) + }) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt new file mode 100644 index 00000000..aa80e256 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt @@ -0,0 +1,43 @@ +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.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +class PacketParticleBatchS2C( + val type: ParticleOptions, + val positions: List, + val velocity: Vec3, +) : CustomPacketPayload { + 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 = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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) + }) + } + + override fun type(): CustomPacketPayload.Type = payloadID +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt new file mode 100644 index 00000000..87c89bef --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt @@ -0,0 +1,37 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +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 +) : CustomPacketPayload { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_rotate") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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) + }) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt new file mode 100644 index 00000000..9359aa65 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt @@ -0,0 +1,48 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import java.util.UUID + + +class PacketParticleCompositionS2C(val uuid: UUID, val type: String, val data: ByteArray) : CustomPacketPayload { + + /** + * 是否是因为距离过长而移除 + */ + var distanceRemove = false + var recreate = false + + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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 + } + }) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt new file mode 100644 index 00000000..6ef54a62 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt @@ -0,0 +1,47 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +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, +) : CustomPacketPayload { + companion object { + private val identifierID = + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_state") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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(), + ) + }) + } + + override fun type(): CustomPacketPayload.Type = payloadID +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt new file mode 100644 index 00000000..c70f2a1f --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt @@ -0,0 +1,73 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleEmittersS2C( + val emitterID: String, + val emitterUUID: UUID, + val emitterData: ByteArray, + val type: PacketType +) : + CustomPacketPayload { + /** + * 发射器同步包的生命周期操作。 + * + * [CREATE] 用于首次进入玩家可见范围,客户端必须创建并启动新实例; + * [CHANGE] 只更新已经存在的客户端实例,不得重新执行启动逻辑; + * [REMOVE] 按 UUID 立即清理客户端实例,不携带完整发射器状态。 + * 数字 ID 是网络协议的一部分,其中 `0` 和 `1` 保留旧协议的创建与移除编号。 + */ + 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 = CustomPacketPayload.Type(id) + + val CODEC = + StreamCodec.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) + ) + }) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt new file mode 100644 index 00000000..153a4080 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt @@ -0,0 +1,106 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.particles.control.ControlType +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +/** + * 三个参数 + * @param uuid 更改的粒子组的UUID + * @param type 三种类型对应三种不同的包参数 + * 1. REMOVE args什么都不写 + * 2. CREATE args输入 + * 1. pos: Vec3d + * 2. groupType: Class + * 3. currentTick: Int + * 4. maxTick: Int + * 5. ByteBuf 用于ControlableParticleGroup 构建的输入参数(不包括自身的UUID) + * 6. scale 缩放大小 + * 3. CHANGE args输入支持 + * 1. pos: Vec3d + * 2. rotateTo: Vec3d + * 3. rotateAxis: Double / Float + * 4. axis: Vec3d + * 5. currentTick: Int + * 6. maxTick: Int + * 7. invoke: 无参方法名 + * 8. scale 缩放大小 + */ +class PacketParticleGroupS2C( + val uuid: UUID, + val type: ControlType, + val args: Map> +) : CustomPacketPayload { + 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" -> return POS + "current_tick" -> return CURRENT_TICK + "max_tick" -> return MAX_TICK + "rotate_to" -> return ROTATE_TO + "rotate_axis" -> return ROTATE_AXIS + "invoke" -> return INVOKE + "scale" -> return SCALE + "groupType" -> return GROUP_TYPE + else -> INVOKE + } + } + } + } + + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_group") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeInt(packet.type.id) + packet.args.forEach { (t, u) -> + val encode = 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 = 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 = ParticleControlerDataBuffers.decodeToBuffer(value) + args[key] = decode + } + return@codec PacketParticleGroupS2C( + uuid, type, args + ) + } + ) + + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt new file mode 100644 index 00000000..e0c78a7d --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt @@ -0,0 +1,40 @@ +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.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +/** + * + */ +class PacketParticleS2C( + val type: ParticleOptions, + val pos: Vec3, + val velocity: Vec3, +) : CustomPacketPayload { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ packet, buf -> + buf.writeVec3(packet.pos) + buf.writeVec3(packet.velocity) + ParticleTypes.STREAM_CODEC.encode(buf, packet.type) + }, { buf -> + val pos = buf.readVec3() + val velocity = buf.readVec3() + val type = ParticleTypes.STREAM_CODEC.decode(buf) + return@codec PacketParticleS2C(type, pos, velocity) + }) + + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt new file mode 100644 index 00000000..41faef4c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt @@ -0,0 +1,57 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.particles.control.ControlType +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketParticleStyleS2C( + val uuid: UUID, + val type: ControlType, + val args: Map> +) : CustomPacketPayload { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_style") + val payloadID = CustomPacketPayload.Type(identifierID) + val CODEC: StreamCodec = + CustomPacketPayload.codec({ packet, buf -> + buf.writeUUID(packet.uuid) + buf.writeInt(packet.type.id) + buf.writeInt(packet.args.size) + packet.args.forEach { (t, u) -> + val encode = 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 = ControlType.getTypeById(id) + val argsCount = buf.readInt() + repeat(argsCount) { + val len = buf.readInt() + val key = buf.readUtf() + val buf = buf.readBytes(len) + val decode = ParticleControlerDataBuffers.decodeToBuffer(buf) + args[key] = decode + } + return@codec PacketParticleStyleS2C( + uuid, type, args + ) + } + ) + + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt new file mode 100644 index 00000000..ecf56ca6 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt @@ -0,0 +1,60 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation +import java.util.UUID + +class PacketRenderEntityS2C(var uuid: UUID, var entityData: ByteArray, var id: ResourceLocation, var method: Method) : + CustomPacketPayload { + 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 = CustomPacketPayload.Type(identifierID) + + @JvmStatic + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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) + val packet = PacketRenderEntityS2C(uuid, bytes, id, Method.idOf(method)) + return@codec packet + } + ) + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt new file mode 100644 index 00000000..064a32ca --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt @@ -0,0 +1,92 @@ +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 net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +class PacketRendererPostEffectS2C private constructor( + internal val operation: Operation, + internal val state: SyncedPostEffectState?, + internal val instanceId: String +) : CustomPacketPayload { + 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 = CustomPacketPayload.Type(identifierID) + + @JvmStatic + val CODEC: StreamCodec = + CustomPacketPayload.codec({ 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: FriendlyByteBuf) { + 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: FriendlyByteBuf): 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() + ) + } + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt new file mode 100644 index 00000000..ab31292f --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt @@ -0,0 +1,243 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +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 +) : CustomPacketPayload { + 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 = CustomPacketPayload.Type(identifierID) + + private val emptySound = ResourceLocation.withDefaultNamespace("empty") + + val CODEC: StreamCodec = CustomPacketPayload.codec( + { 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: FriendlyByteBuf, values: Set) { + buf.writeVarInt(values.size) + values.forEach(buf::writeResourceLocation) + } + + private fun readResourceLocationSet(buf: FriendlyByteBuf): Set { + val result = LinkedHashSet() + repeat(buf.readVarInt()) { + result.add(buf.readResourceLocation()) + } + return result + } + + private fun writeSoundSourceSet(buf: FriendlyByteBuf, values: Set) { + buf.writeVarInt(values.size) + values.forEach(buf::writeEnum) + } + + private fun readSoundSourceSet(buf: FriendlyByteBuf): Set { + val result = LinkedHashSet() + repeat(buf.readVarInt()) { + result.add(buf.readEnum(SoundSource::class.java)) + } + return result + } + + private fun writeStringSet(buf: FriendlyByteBuf, values: Set) { + buf.writeVarInt(values.size) + values.forEach(buf::writeUtf) + } + + private fun readStringSet(buf: FriendlyByteBuf): Set { + val result = LinkedHashSet() + repeat(buf.readVarInt()) { + result.add(buf.readUtf()) + } + return result + } + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt new file mode 100644 index 00000000..6b1db22e --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt @@ -0,0 +1,83 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +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 +) : CustomPacketPayload { + companion object { + private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_loop") + val payloadID = CustomPacketPayload.Type(identifierID) + + val CODEC: StreamCodec = CustomPacketPayload.codec( + { 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 + ) + } + } + + override fun type(): CustomPacketPayload.Type { + return payloadID + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt new file mode 100644 index 00000000..545139bb --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt @@ -0,0 +1,440 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +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.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation + +/** + * 从服务端向客户端同步一个地形效果组的替换或增量更新。 + * + * 协议支持完整替换、追加位置、移除位置、替换 uniform 和删除整组。位置使用相对首坐标的 ZigZag + * 差值编码,生效时间使用相对首个绝对 tick 的差值编码,避免大批连续方块重复传输完整坐标和时间。 + * 包实例只由 companion object 中的工厂创建,并由 + * [cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectManager] 发送。 + */ +@CooAutoRegister +class PacketTerrainEffectGroupS2C() : CooPacket() { + /** 当前包的协议操作类型。 */ + var operation: Int = REPLACE + + /** 目标效果组所属的维度 ID。 */ + var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") + + /** 目标效果组 ID。 */ + var groupId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + + /** 完整替换操作引用的方块 Pipeline ID。 */ + var pipelineId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") + + /** 完整替换操作中组开始的绝对游戏 tick。 */ + var startedAt: Long = 0L + + /** 完整替换操作中组到期的绝对游戏 tick;`null` 表示持久组。 */ + var expiresAt: Long? = null + + /** 完整替换操作中用于重叠组排序的服务端序号。 */ + var sequence: Long = 0L + + /** 用于丢弃重复或乱序更新的单调协议版本号。 */ + var revision: Long = 0L + + /** 完整替换和 options 更新中的有符号绘制优先级。 */ + var priority: Int = 0 + + /** 完整替换和 options 更新中的合成方式。 */ + var composition: CooTerrainEffectComposition = CooTerrainEffectComposition.REPLACE + + /** + * 操作涉及的位置数据。 + * + * 完整替换和追加操作中,键为方块坐标,值为绝对生效 tick;移除位置操作只使用键。 + */ + var positions: Map = emptyMap() + + /** 键为 shader uniform 名称,值为完整替换或 uniform 更新携带的数据。 */ + var uniforms: Map = emptyMap() + + /** @return 地形效果组服务端到客户端协议的固定资源 ID。 */ + override fun id(): ResourceLocation = ResourceLocation.fromNamespaceAndPath( + CooParticlesConstants.MOD_ID, + PACKET_ID + ) + + /** @return 编解码本协议全部操作类型的 StreamCodec。 */ + override fun codec(): StreamCodec = CODEC + + /** + * 在客户端主线程把本包更新应用到 [CooTerrainEffectRegistry]。 + * + * @param context 当前客户端网络处理上下文 + */ + override fun onClientReceive(context: 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 + ) + } + } + } + + /** 创建各协议操作并实现紧凑的坐标、时间和 uniform 编解码。 */ + 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 + + private val CODEC: StreamCodec = + StreamCodec.of(::encode, ::decode) + + /** + * 创建完整替换效果组的协议包。 + * + * @param snapshot 要同步的完整服务端效果组快照 + * @return 携带 Pipeline、时间、位置和 uniform 的替换包 + */ + 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 + } + } + + /** + * 创建只追加位置和绝对生效时间的协议包。 + * + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @param positions 键为新增方块坐标,值为绝对生效 tick + * @return 位置追加包 + */ + 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 + } + } + + /** + * 创建删除整个效果组的协议包。 + * + * @param dimension 组所属维度 ID + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @return 整组删除包 + */ + 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 + } + } + + /** + * 创建只删除组内指定位置的协议包。 + * + * @param dimension 组所属维度 ID + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @param positions 要删除的方块坐标 + * @return 位置删除包 + */ + 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 } + } + } + + /** + * 创建替换整组 uniform 集合的协议包。 + * + * @param dimension 组所属维度 ID + * @param groupId 组 ID + * @param revision 本次更新的单调协议版本号 + * @param uniforms 键为 shader uniform 名称,值为新的完整 uniform 数据 + * @return uniform 更新包 + */ + 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: FriendlyByteBuf, 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: FriendlyByteBuf): 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 + } + + /** + * 以首项为坐标和时间基准写入一组位置。 + * + * @param buffer 目标网络缓冲 + * @param positions 键为方块坐标,值为该位置的绝对生效 tick + */ + private fun writeTimedPositions(buffer: FriendlyByteBuf, 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)) + } + } + + /** + * 读取以首项为基准编码的位置和绝对生效时间。 + * + * @param buffer 源网络缓冲 + * @return 键为方块坐标、值为绝对生效 tick 的映射 + */ + private fun readTimedPositions(buffer: FriendlyByteBuf): 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: FriendlyByteBuf, 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)) + } + } + + /** @return 从相对坐标差值还原出的方块位置集合。 */ + private fun readPositions(buffer: FriendlyByteBuf): 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()) + ) + ) + } + } + } + + /** + * 写入带类型标记的 uniform 映射。 + * + * @param buffer 目标网络缓冲 + * @param uniforms 键为 uniform 名称,值为浮点数、整数或向量数据 + */ + private fun writeUniforms(buffer: FriendlyByteBuf, uniforms: Map) { + buffer.writeVarInt(uniforms.size) + uniforms.forEach { (name, value) -> + buffer.writeUtf(name) + CooUniformValue.STREAM_CODEC.encode(buffer, value) + } + } + + /** + * 读取带类型标记的 uniform 映射。 + * + * @param buffer 源网络缓冲 + * @return 键为 uniform 名称、值为对应类型数据的映射 + */ + private fun readUniforms(buffer: FriendlyByteBuf): 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt new file mode 100644 index 00000000..3fe7248c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt @@ -0,0 +1,186 @@ +package cn.coostack.cooparticlesapi.network.packet.server + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +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.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation + +/** + * 程序化 terrain mapping 的服务端到客户端同步包。 + * + * 包只携带区域参数和实例参数,不携带 BlockPos、非空气方块或高度图结果。 + */ +@CooAutoRegister +class PacketTerrainMappingS2C() : CooPacket() { + 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(): StreamCodec = CODEC + + override fun onClientReceive(context: 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 + private val CODEC: StreamCodec = + StreamCodec.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: FriendlyByteBuf, 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: FriendlyByteBuf): 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: FriendlyByteBuf, uniforms: Map) { + buffer.writeVarInt(uniforms.size) + uniforms.forEach { (name, value) -> + buffer.writeUtf(name) + CooUniformValue.STREAM_CODEC.encode(buffer, value) + } + } + + private fun readUniforms(buffer: FriendlyByteBuf): Map { + val count = buffer.readVarInt() + val result = LinkedHashMap(count) + repeat(count) { + result[buffer.readUtf()] = CooUniformValue.STREAM_CODEC.decode(buffer) + } + return result + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt new file mode 100644 index 00000000..d010466c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt @@ -0,0 +1,17 @@ +package cn.coostack.cooparticlesapi.network.packet.server.listener + +import cn.coostack.cooparticlesapi.event.CooEventBus +import cn.coostack.cooparticlesapi.event.events.key.KeyActionEvent +import cn.coostack.cooparticlesapi.network.packet.client.PacketKeyActionC2S +import cn.coostack.cooparticlesapi.platform.network.ServerContext + +object ServerKeyActionHandler { + fun receive(packet: PacketKeyActionC2S, context: ServerContext) { + val player = context.player() + context.server().execute { + CooEventBus.call( + KeyActionEvent(player, packet.keyActions, true) + ) + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt new file mode 100644 index 00000000..bc7b67d0 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt @@ -0,0 +1,38 @@ +package cn.coostack.cooparticlesapi.network.packet.status + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.performance.PerformanceStatusClientBridge +import cn.coostack.cooparticlesapi.performance.PerformanceStatusControlAction +import net.minecraft.resources.ResourceLocation + +/** 服务端命令向目标玩家客户端发送的 Status 会话控制包。 */ +@CooAutoRegister +class PacketPerformanceStatusControlS2C() : CooPacket() { + /** 网络动作名称。 */ + @CodecField var action: String = "" + + /** 使用类型安全动作创建控制包。 */ + constructor(action: PerformanceStatusControlAction) : this() { + this.action = action.name + } + + /** 返回该业务包的稳定协议 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** 经纯 common bridge 把动作交给客户端初始化时安装的控制器。 */ + override fun onClientReceive(context: ClientContext) { + PerformanceStatusControlAction.fromNetworkName(action)?.let(PerformanceStatusClientBridge::handle) + } + + /** 协议注册常量。 */ + companion object { + /** 业务包稳定注册 ID,属于网络协议的一部分。 */ + private const val PACKET_ID = "performance_status_control_s2c" + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt new file mode 100644 index 00000000..feb102c0 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt @@ -0,0 +1,39 @@ +package cn.coostack.cooparticlesapi.network.packet.status + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.CooPacketKind +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerRequestGate +import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshotFactory +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.resources.ResourceLocation + +/** 客户端在 Status 活跃期间按配置间隔发送的无参数服务端快照请求。 */ +@CooAutoRegister +class PacketPerformanceStatusRequestC2S : CooPacket() { + /** 返回该业务包的稳定协议 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** 验证请求类型、命令权限和服务端限流后构造固定大小快照。 */ + override fun onServerReceive(context: ServerContext) { + if (context.kind != CooPacketKind.REQUEST) return + val refreshInterval = CooParticlesServices.API_CONFIG_MANAGER.getConfig() + .statusServerRefreshIntervalTicks + if (!PerformanceStatusServerRequestGate.tryAcquire(context.sender, refreshInterval)) return + context.reply( + PacketPerformanceStatusSnapshotS2C.from( + PerformanceStatusServerSnapshotFactory.create(context.sender.server) + ) + ) + } + + /** 协议注册常量。 */ + companion object { + /** 业务包稳定注册 ID,属于网络协议的一部分。 */ + private const val PACKET_ID = "performance_status_request_c2s" + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt new file mode 100644 index 00000000..7094ae10 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt @@ -0,0 +1,191 @@ +package cn.coostack.cooparticlesapi.network.packet.status + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkTotals +import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshot +import cn.coostack.cooparticlesapi.performance.PerformanceStatusVanillaPacketTotals +import net.minecraft.resources.ResourceLocation + +/** 服务端返回给单个查看者的固定字段 Status 快照。 */ +@CooAutoRegister +class PacketPerformanceStatusSnapshotS2C : CooPacket() { + /** 服务端平均 MSPT。 */ + @CodecField var averageMspt: Double = 0.0 + + /** 服务端 Barrage 数。 */ + @CodecField var barrages: Int = 0 + + /** 服务端生成快照的 epoch 毫秒。 */ + @CodecField var capturedAtEpochMillis: Long = 0L + + /** 服务端 CooFX scene 数。 */ + @CodecField var cooFxScenes: Int = 0 + + /** 服务端 Composition 数。 */ + @CodecField var compositions: Int = 0 + + /** 服务端 DisplayEntity 数。 */ + @CodecField var displayEntities: Int = 0 + + /** 服务端 Emitter 数。 */ + @CodecField var emitters: Int = 0 + + /** 服务端 JVM 累计垃圾收集次数。 */ + @CodecField var gcCollectionCount: Long = 0L + + /** 服务端 JVM 累计垃圾收集耗时毫秒。 */ + @CodecField var gcCollectionTimeMs: Long = 0L + + /** 服务端最大堆字节数。 */ + @CodecField var heapMaxBytes: Long = 0L + + /** 服务端已使用堆字节数。 */ + @CodecField var heapUsedBytes: Long = 0L + + /** 服务端历史最大 MSPT。 */ + @CodecField var maxMspt: Double = 0.0 + + /** 服务端累计收到的 CooPacket 业务字节数。 */ + @CodecField var networkReceivedBytes: Long = 0L + + /** 服务端累计收到的 CooPacket 数。 */ + @CodecField var networkReceivedPackets: Long = 0L + + /** 服务端累计发送的 CooPacket 业务字节数。 */ + @CodecField var networkSentBytes: Long = 0L + + /** 服务端累计发送的 CooPacket 数。 */ + @CodecField var networkSentPackets: Long = 0L + + /** 当前在线玩家数。 */ + @CodecField var onlinePlayers: Int = 0 + + /** 服务端历史 P95 MSPT。 */ + @CodecField var p95Mspt: Double = 0.0 + + /** 服务端旧 ParticleGroup 数。 */ + @CodecField var particleGroups: Int = 0 + + /** 服务端配置的 Status 快照刷新间隔。 */ + @CodecField var refreshIntervalTicks: Int = 20 + + /** 服务端 RenderEntity 数。 */ + @CodecField var renderEntities: Int = 0 + + /** 服务端当前 tick 序号。 */ + @CodecField var serverTick: Long = 0L + + /** 服务端管理的声音实例数。 */ + @CodecField var soundInstances: Int = 0 + + /** 服务端管理的循环声音数。 */ + @CodecField var soundLoops: Int = 0 + + /** 当前目标 TPS。 */ + @CodecField var targetTps: Double = 20.0 + + /** 服务端 Terrain effect group 数。 */ + @CodecField var terrainEffectGroups: Int = 0 + + /** 服务端 Terrain mapping 实例数。 */ + @CodecField var terrainMappings: Int = 0 + + /** 当前可维持 TPS。 */ + @CodecField var tps: Double = 20.0 + + /** 服务端原版 Connection 累计收到的 Packet 数。 */ + @CodecField var vanillaNetworkReceivedPackets: Long = 0L + + /** 服务端原版 Connection 累计发送的 Packet 数。 */ + @CodecField var vanillaNetworkSentPackets: Long = 0L + + /** 返回该业务包的稳定协议 ID。 */ + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) + } + + /** 把协议字段恢复为不可变服务端快照。 */ + fun toSnapshot(): PerformanceStatusServerSnapshot { + return PerformanceStatusServerSnapshot( + capturedAtEpochMillis = capturedAtEpochMillis, + serverTick = serverTick, + refreshIntervalTicks = refreshIntervalTicks.coerceIn(5, 1_200), + targetTps = targetTps, + tps = tps, + averageMspt = averageMspt, + p95Mspt = p95Mspt, + maxMspt = maxMspt, + onlinePlayers = onlinePlayers, + particleGroups = particleGroups, + renderEntities = renderEntities, + displayEntities = displayEntities, + emitters = emitters, + compositions = compositions, + gcCollectionCount = gcCollectionCount, + gcCollectionTimeMs = gcCollectionTimeMs, + terrainEffectGroups = terrainEffectGroups, + terrainMappings = terrainMappings, + soundInstances = soundInstances, + soundLoops = soundLoops, + barrages = barrages, + cooFxScenes = cooFxScenes, + heapUsedBytes = heapUsedBytes, + heapMaxBytes = heapMaxBytes, + cooPackets = PerformanceStatusNetworkTotals( + sentPackets = networkSentPackets, + sentBytes = networkSentBytes, + receivedPackets = networkReceivedPackets, + receivedBytes = networkReceivedBytes, + ), + vanillaPackets = PerformanceStatusVanillaPacketTotals( + sentPackets = vanillaNetworkSentPackets, + receivedPackets = vanillaNetworkReceivedPackets, + ), + ) + } + + /** 协议注册常量和快照转换入口。 */ + companion object { + /** 业务包稳定注册 ID,属于网络协议的一部分。 */ + private const val PACKET_ID = "performance_status_snapshot_s2c" + + /** 从不可变服务端快照创建可反射编码的业务包。 */ + fun from(snapshot: PerformanceStatusServerSnapshot): PacketPerformanceStatusSnapshotS2C { + return PacketPerformanceStatusSnapshotS2C().also { packet -> + packet.averageMspt = snapshot.averageMspt + packet.barrages = snapshot.barrages + packet.capturedAtEpochMillis = snapshot.capturedAtEpochMillis + packet.cooFxScenes = snapshot.cooFxScenes + packet.compositions = snapshot.compositions + packet.displayEntities = snapshot.displayEntities + packet.emitters = snapshot.emitters + packet.gcCollectionCount = snapshot.gcCollectionCount + packet.gcCollectionTimeMs = snapshot.gcCollectionTimeMs + packet.heapMaxBytes = snapshot.heapMaxBytes + packet.heapUsedBytes = snapshot.heapUsedBytes + packet.maxMspt = snapshot.maxMspt + packet.networkReceivedBytes = snapshot.cooPackets.receivedBytes + packet.networkReceivedPackets = snapshot.cooPackets.receivedPackets + packet.networkSentBytes = snapshot.cooPackets.sentBytes + packet.networkSentPackets = snapshot.cooPackets.sentPackets + packet.onlinePlayers = snapshot.onlinePlayers + packet.p95Mspt = snapshot.p95Mspt + packet.particleGroups = snapshot.particleGroups + packet.refreshIntervalTicks = snapshot.refreshIntervalTicks + packet.renderEntities = snapshot.renderEntities + packet.serverTick = snapshot.serverTick + packet.soundInstances = snapshot.soundInstances + packet.soundLoops = snapshot.soundLoops + packet.targetTps = snapshot.targetTps + packet.terrainEffectGroups = snapshot.terrainEffectGroups + packet.terrainMappings = snapshot.terrainMappings + packet.tps = snapshot.tps + packet.vanillaNetworkReceivedPackets = snapshot.vanillaPackets.receivedPackets + packet.vanillaNetworkSentPackets = snapshot.vanillaPackets.sentPackets + } + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt new file mode 100644 index 00000000..ba8c6d2c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt @@ -0,0 +1,48 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.BoundControllerEntry +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketOpenBoundTestSelectionScreenS2C() : CooPacket() { + @CodecField var currentIndices: List = emptyList() + @CodecField var currentOptionIds: List = emptyList() + @CodecField var dimensions: List = emptyList() + @CodecField var groupIds: List = emptyList() + @CodecField var loaded: List = emptyList() + @CodecField var optionCounts: List = emptyList() + @CodecField var positions: List = emptyList() + @CodecField var running: List = emptyList() + @CodecField var statuses: List = emptyList() + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_bound_test_selection_screen_s2c") + } + + override fun onClientReceive(context: ClientContext) { + TestControllerClientScreens.openBoundSelection(this) + } + + companion object { + fun fromEntries(entries: List): PacketOpenBoundTestSelectionScreenS2C { + return PacketOpenBoundTestSelectionScreenS2C().also { + it.dimensions = entries.map { entry -> entry.dimension } + it.positions = entries.map { entry -> entry.pos } + it.groupIds = entries.map { entry -> entry.groupId } + it.statuses = entries.map { entry -> entry.status } + it.currentIndices = entries.map { entry -> entry.currentIndex } + it.currentOptionIds = entries.map { entry -> entry.currentOptionId } + it.optionCounts = entries.map { entry -> entry.optionCount } + it.loaded = entries.map { entry -> entry.loaded } + it.running = entries.map { entry -> entry.running } + } + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt new file mode 100644 index 00000000..3139e246 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt @@ -0,0 +1,123 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockEntity +import cn.coostack.cooparticlesapi.network.packet.api.ClientContext +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens +import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec +import cn.coostack.cooparticlesapi.test.api.TestOptionParamSpec +import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketOpenTestControllerScreenS2C() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var boxDepth: Double = 0.6 + @CodecField var boxHeight: Double = 1.8 + @CodecField var boxWidth: Double = 0.6 + @CodecField var currentIndex: Int = 0 + @CodecField var dimension: String = "" + @CodecField var forwardX: Double = 0.0 + @CodecField var forwardY: Double = 0.0 + @CodecField var forwardZ: Double = 1.0 + @CodecField var forwardDynamic: Boolean = false + @CodecField var forwardTrack: String = "" + @CodecField var groupId: String = "" + @CodecField var mode: String = "sequential" + @CodecField var offsetX: Double = 0.0 + @CodecField var offsetY: Double = 0.0 + @CodecField var offsetZ: Double = 0.0 + @CodecField var optionCount: Int = 0 + @CodecField var optionIds: List = emptyList() + @CodecField var optionParamSpecs: List = emptyList() + @CodecField var optionParamValues: List = emptyList() + @CodecField var pendingReview: Boolean = false + @CodecField var positionDynamic: Boolean = false + @CodecField var positionTrack: String = "" + @CodecField var registeredIds: List = emptyList() + @CodecField var registeredOptionIds: List = emptyList() + @CodecField var registeredOptionParamSpecs: List> = emptyList() + @CodecField var repeatDelayTicks: Int = 0 + @CodecField var repeatIndex: Boolean = false + @CodecField var running: Boolean = false + @CodecField var selectedIndex: Int = 0 + @CodecField var status: String = "" + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_test_controller_screen_s2c") + } + + override fun onClientReceive(context: ClientContext) { + TestControllerClientScreens.openController(this) + } + + companion object { + /** + * 从服务端方块实体创建控制界面快照。 + * + * @param dimension 方块所在维度的资源 ID 文本 + * @param blockEntity 待同步的测试控制器方块实体 + * @param registeredIds 当前可用的方块测试组资源 ID + * @return 可发送到客户端的完整界面数据包 + */ + fun fromBlockEntity( + dimension: String, + blockEntity: TestControllerBlockEntity, + registeredIds: List + ): PacketOpenTestControllerScreenS2C { + return PacketOpenTestControllerScreenS2C().also { + it.dimension = dimension + it.blockPos = blockEntity.blockPos + it.groupId = blockEntity.groupId + it.mode = blockEntity.mode.id + it.selectedIndex = blockEntity.selectedIndex + it.repeatIndex = blockEntity.repeatIndex + it.repeatDelayTicks = blockEntity.repeatDelayTicks + it.offsetX = blockEntity.playerOffset.x + it.offsetY = blockEntity.playerOffset.y + it.offsetZ = blockEntity.playerOffset.z + it.forwardX = blockEntity.playerForward.x + it.forwardY = blockEntity.playerForward.y + it.forwardZ = blockEntity.playerForward.z + it.forwardDynamic = blockEntity.playerForwardDynamic + it.forwardTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerForwardTrack) + it.positionDynamic = blockEntity.playerPositionDynamic + it.positionTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerPositionTrack) + it.boxWidth = blockEntity.playerBoxWidth + it.boxHeight = blockEntity.playerBoxHeight + it.boxDepth = blockEntity.playerBoxDepth + it.running = blockEntity.isRunning() + it.status = blockEntity.statusText() + it.optionCount = blockEntity.optionCount() + it.optionIds = ArrayList(blockEntity.optionIds()) + it.optionParamSpecs = encodeOptionParamSpecs(blockEntity.optionParamSpecs()) + it.optionParamValues = TestOptionParamCodec.encodeAllOptionValues(blockEntity.optionParamValues()) + it.pendingReview = blockEntity.hasPendingReview() + it.currentIndex = blockEntity.currentIndex() + it.registeredIds = registeredIds.map(ResourceLocation::toString) + it.registeredOptionIds = ArrayList(registeredIds.map { id -> + encodeOptionIds(blockEntity.optionIds(id)) + }) + it.registeredOptionParamSpecs = ArrayList(registeredIds.map { id -> + encodeOptionParamSpecs(blockEntity.optionParamSpecs(id)) + }) + } + } + + private fun encodeOptionIds(optionIds: List): String { + return optionIds.joinToString(OPTION_ID_SEPARATOR) + } + + private fun encodeOptionParamSpecs(optionSpecs: List>>): List { + return TestOptionParamCodec.encodeAllOptionSpecs(optionSpecs).map { encoded -> + encoded.encodeToByteArray() + } + } + + const val OPTION_ID_SEPARATOR: String = "\u001F" + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt new file mode 100644 index 00000000..3d3ac4b0 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import cn.coostack.cooparticlesapi.test.block.BlockTestOptionResult +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketReviewTestControllerC2S() : CooPacket() { + @CodecField var action: String = "" + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos, action: String) : this() { + this.dimension = dimension + this.blockPos = blockPos + this.action = action + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "review_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + val result = when { + action.equals(PASS, ignoreCase = true) -> BlockTestOptionResult.PASSED + action.equals(FAIL, ignoreCase = true) -> BlockTestOptionResult.FAILED + action.equals(SKIP, ignoreCase = true) -> BlockTestOptionResult.SKIPPED + else -> return + } + TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.reviewCurrent(result) + } + + companion object { + const val PASS = "pass" + const val FAIL = "fail" + const val SKIP = "skip" + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt new file mode 100644 index 00000000..39c2c610 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketSelectBoundTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos) : this() { + this.dimension = dimension + this.blockPos = blockPos + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "select_bound_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return + TestControllerBlockAccess.openControllerScreen(sender, blockEntity) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt new file mode 100644 index 00000000..39a58572 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketStartTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos) : this() { + this.dimension = dimension + this.blockPos = blockPos + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "start_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.startTest() + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt new file mode 100644 index 00000000..a0389b03 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt @@ -0,0 +1,31 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation + +@CooAutoRegister +class PacketStopTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var dimension: String = "" + + constructor(dimension: String, blockPos: BlockPos) : this() { + this.dimension = dimension + this.blockPos = blockPos + } + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "stop_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.stopTest() + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt new file mode 100644 index 00000000..5fe02c17 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt @@ -0,0 +1,80 @@ +package cn.coostack.cooparticlesapi.network.packet.testblock + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CodecField +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess +import cn.coostack.cooparticlesapi.network.packet.api.CooPacket +import cn.coostack.cooparticlesapi.network.packet.api.ServerContext +import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec +import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec +import cn.coostack.cooparticlesapi.test.block.BlockTestMode +import net.minecraft.core.BlockPos +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.phys.Vec3 + +@CooAutoRegister +class PacketUpdateTestControllerC2S() : CooPacket() { + @CodecField var blockPos: BlockPos = BlockPos.ZERO + @CodecField var boxDepth: Double = 0.6 + @CodecField var boxHeight: Double = 1.8 + @CodecField var boxWidth: Double = 0.6 + @CodecField var dimension: String = "" + @CodecField var forwardX: Double = 0.0 + @CodecField var forwardY: Double = 0.0 + @CodecField var forwardZ: Double = 1.0 + @CodecField var forwardDynamic: Boolean = false + @CodecField var forwardTrack: String = "" + @CodecField var groupId: String = "" + @CodecField var mode: String = "sequential" + @CodecField var offsetX: Double = 0.0 + @CodecField var offsetY: Double = 0.0 + @CodecField var offsetZ: Double = 0.0 + @CodecField var optionParamIndex: Int = 0 + @CodecField var optionParamValues: String = "" + @CodecField var positionDynamic: Boolean = false + @CodecField var positionTrack: String = "" + @CodecField var repeatDelayTicks: Int = 0 + @CodecField var repeatIndex: Boolean = false + @CodecField var reopen: Boolean = false + @CodecField var selectedIndex: Int = 0 + + override fun id(): ResourceLocation { + return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "update_test_controller_c2s") + } + + override fun onServerReceive(context: ServerContext) { + val sender = context.sender + if (!sender.isCreative) return + val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return + val wasRunning = blockEntity.isRunning() + // 待人工复核时重启会丢弃尚未给出的复核结果,交由随后的复核包决定测试组去向。 + val pendingReview = blockEntity.hasPendingReview() + val offset = Vec3(offsetX, offsetY, offsetZ) + val forward = Vec3(forwardX, forwardY, forwardZ) + val changed = blockEntity.updateConfig( + groupId = groupId, + mode = BlockTestMode.fromId(mode), + selectedIndex = selectedIndex, + repeatIndex = repeatIndex, + repeatDelayTicks = repeatDelayTicks, + playerOffset = offset, + playerForward = forward, + playerPositionDynamic = positionDynamic, + playerForwardDynamic = forwardDynamic, + playerPositionTrack = BlockTestAnimationTrackCodec.decode(positionTrack, offset), + playerForwardTrack = BlockTestAnimationTrackCodec.decode(forwardTrack, forward), + playerBoxWidth = boxWidth, + playerBoxHeight = boxHeight, + playerBoxDepth = boxDepth, + optionParamIndex = optionParamIndex, + optionParamValues = TestOptionParamCodec.decodeOptionValues(optionParamValues) + ) + if (wasRunning && changed && !pendingReview) { + blockEntity.startTest() + } + if (reopen) { + TestControllerBlockAccess.openControllerScreen(sender, blockEntity) + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt new file mode 100644 index 00000000..2f0ae949 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt @@ -0,0 +1,164 @@ +package cn.coostack.cooparticlesapi.network.particle + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.utils.MathDataUtil + +/** + * 由于服务端获取不到客户层的粒子总数 + * 所以这里不做粒子上限 + * 但是记录是用于 其他客户端同步 + * + * 如果要设定自动同步 + * 在 otherPacketArgs 设置 "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount) + * 或者 在这个方法中 return mapOf(toggleArgLeastIndex(),toggleArgStatus(),...其他自定义的数据) + * + * 你无需在 SequencedServerParticleGroup的Provider中 手动应用toggle输入的count + * 服务器会自动处理此参数 + * + * 数据包设定 + * addCount args type int -> count + * removeCount args type int -> count + * toggle args type int-> count + */ +@Deprecated("使用ParticleComposition") +abstract class SequencedServerParticleGroup(visibleRange: Double = 32.0) : ServerParticleGroup(visibleRange) { + /** + * 记录了每一个粒子的显示状态 (只要maxCount确实等于loadParticleLocationsWithIndex返回的map的size) + */ + private val clientIndexStatus = LongArray((maxCount() / 64) + 1) + + var serverSequencedParticleCount = 0 + + /** + * 切记一定要和 SequencedParticleGroup.loadParticleLocationsWithIndex().size 相同 + * + * 如果你的group的粒子数量是可变的(使用了flush方法刷新了粒子样式 其中长度发生变化) + * + * 那么请在服务器层做好数据同步 ( size同步 ) + * + * 如果此处的 maxCount > SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致数组越界异常 + * + * 如果此处的 maxCount < SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致粒子控制不完全(部分粒子无法从服务器生成) + * + * @return 应当返回 SequencedParticleGroup.loadParticleLocationsWithIndex().size + */ + abstract fun maxCount(): Int + + fun isDisplayed(index: Int): Boolean { + val storage = MathDataUtil.getStoragePageLong(index) + if (storage >= clientIndexStatus.size) { + return false + } + + return MathDataUtil.getStorageWithBitLong( + MathDataUtil.getStorageWithBitLong(index) + ) == 1 + } + + fun setDisplayed(index: Int, status: Boolean) { + val storage = MathDataUtil.getStorageWithBitLong(index) + val page = MathDataUtil.getStoragePageLong(index) + if (page >= clientIndexStatus.size) { + return + } + clientIndexStatus[page] = MathDataUtil.setStatusLong(clientIndexStatus[page], storage, status) + } + + fun toggleArgLeastIndex(): Pair> { + return "toggle" to ParticleControlerDataBuffers.int(1) + } + + fun toggleArgStatus(): Pair> { + return "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) + } + + fun addSingle() { + if (serverSequencedParticleCount >= maxCount()) { + return + } + change( + { + setDisplayed(serverSequencedParticleCount++, true) + }, + mapOf( + "addCount" to ParticleControlerDataBuffers.int(1) + ) + ) + } + + fun addMultiple(count: Int) { + change( + { + for (index in serverSequencedParticleCount until serverSequencedParticleCount + count) { + setDisplayed(index, true) + } + serverSequencedParticleCount += count + }, + mapOf( + "addCount" to ParticleControlerDataBuffers.int(count) + ) + ) + if (serverSequencedParticleCount >= maxCount()) { + serverSequencedParticleCount = maxCount() + } + } + + fun removeSingle(): Boolean { + if (serverSequencedParticleCount <= 0) { + return false + } + change( + { + setDisplayed(serverSequencedParticleCount--, false) + }, + mapOf( + "removeCount" to ParticleControlerDataBuffers.int(1) + ) + ) + return true + } + + fun removeMultiple(count: Int) { + if (serverSequencedParticleCount <= 0) { + return + } + change( + { + for (index in serverSequencedParticleCount - count + 1..serverSequencedParticleCount) { + setDisplayed(index, false) + } + serverSequencedParticleCount -= count + if (serverSequencedParticleCount <= 0) { + serverSequencedParticleCount = 0 + } + }, + mapOf( + "removeCount" to ParticleControlerDataBuffers.int(count) + ) + ) + } + + fun changeSingle(index: Int, status: Boolean) { + if (index >= maxCount() || index < 0) { + return + } + change( + { + setDisplayed(index, status) + }, mapOf( + "change_single" to ParticleControlerDataBuffers.int(index) + ) + ) + } + + fun toggleCurrentCount() { + change( + {}, + mapOf( + "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount), + "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) + ) + ) + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt new file mode 100644 index 00000000..f515823d --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt @@ -0,0 +1,328 @@ +package cn.coostack.cooparticlesapi.network.particle + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.api.controler.server.ServerControler +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +/** + * @param clientGroup 客户端渲染的类型 + * @param uuid 粒子组合唯一标识符 + * @param visibleRange 玩家可见范围 (origin) + */ +@Deprecated("使用ParticleComposition") + +abstract class ServerParticleGroup( + var visibleRange: Double = 32.0 +) : ServerControler { + val uuid: UUID = UUID.randomUUID() + var pos: Vec3 = Vec3.ZERO + internal set + var world: Level? = null + internal set + internal var valid = true + var canceled = false + internal set + + /** + * 客户端渲染视角下的生命周期 + */ + var clientTick = 0 + internal set + var clientMaxTick = 120 + internal set + var scale = 1.0 + + /** + * 服务器内的生命周期 + * 同步数据时不会传输给客户端 + */ + var tick = 0 + var maxTick = 120 + + + var axis: RelativeLocation = RelativeLocation(0.0, 1.0, 0.0) + internal set + + /** + * 每个tick能做的事情 + * 例如绑定实体位置 旋转等 + */ + abstract fun tick() + + /** + * 获取自定义包的其他参数 (用于服务器传输给客户端) + */ + abstract fun otherPacketArgs(): Map> + + /** + * 获取对应的客户端层Class + * @return null 不能单独生成的类型 + */ + abstract fun getClientType(): Class? + + + /** + * 当粒子组合在服务器创建并且发送到客户端时执行 + * 可以给客户端发送一些包修改 + */ + open fun onGroupDisplay(pos: Vec3, world: ServerLevel) {} + + /** + * 通过doTickAlive函数导致粒子组合到达最终生命周期时执行的函数 + * 不强制重写 + */ + open fun onTickAliveDeath() {} + + /** + * 同步后客户端层面粒子死亡 (除非没开启tickAlive) + * 服务器处理上同样 + */ + open fun onClientViewDeath() {} + + /** + * 可以在abstract tick中执行 + */ + fun doTickClient() { + if (clientTick++ >= clientMaxTick) { + onClientViewDeath() + } + } + + /** + * 可以在abstract tick中执行 + */ + fun doTickAlive() { + if (tick++ >= maxTick) { + kill() + onTickAliveDeath() + } + } + + /** + * 不立刻同步给客户端 + * 只修改参数 + * 但是在新加入可视的玩家会应用此scale + * 如果想要在客户端不应用服务器的scale + * 则需要在provider的create方法中重新修改scale值 + */ + fun scaleOnServer(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale must be greater than zero.") + return + } + this.scale = new + } + + /** + * 服务器修改客户端缩放大小 + * 并且立刻同步到客户端 + */ + fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale must be greater than zero.") + return + } + change( + { + this.scale = new + }, + mapOf( + PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(new) + ) + ) + } + + /** + * 销毁包 (在update时会删除所有的可见) + */ + fun kill() { + canceled = true + world ?: return + val visible = ServerParticleGroupManager.filterVisiblePlayer(this) + visible.forEach { + val player = world!!.getPlayerByUUID(it) ?: return@forEach + // 发销毁包 + val packet = PacketParticleGroupS2C(uuid, ControlType.REMOVE, mapOf()) + CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) + } + } + + /** + * 跟随玩家在线状态. 世界跟换 删除粒子 + */ + fun withPlayerStats(player: ServerPlayer) { + if (player.hasDisconnected()) { + kill() + } + + if (player.level() != world) { + kill() + } + } + + fun withEntityStats(entity: LivingEntity) { + if (entity.isDeadOrDying) { + kill() + } + + if (entity.level() != world) { + kill() + } + + } + + fun setAxis(axis: Vec3) { + change( + { this.axis = RelativeLocation.of(axis) }, mapOf( + PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(axis) + ) + ) + } + + /** + * 不发送到客户端 + */ + fun setPosOnServer(pos: Vec3) { + this.pos = pos + } + + /** + * 同步旋转后的中心点和对称轴 + */ + fun setRotateToOnServer(to: RelativeLocation) { + axis = to.normalize() + } + + + /** + * 出现了一些控制类之外的传送 + */ + fun teleportGroupTo(pos: Vec3) { + // 发包告知 所有可见客户端 + change( + { this.pos = pos }, mapOf( + PacketParticleGroupS2C.PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(pos) + ) + ) + } + + override fun getValue(): ServerParticleGroup { + return this + } + + override fun remove() { + kill() + } + + + override fun teleportTo(to: Vec3) { + teleportGroupTo(to) + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + /** + * 出现了一些控制类之外的旋转 + */ + override fun rotateAsAxis(angle: Double) { + change( + {}, mapOf( + PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( + angle + ) + ) + ) + } + + override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { + change( + {}, mapOf( + PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( + angle + ), + PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d( + to.toVector() + ) + ) + ) + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ServerLevel) return + ServerParticleGroupManager.addParticleGroup(this, pos, world) + } + + /** + * 出现了一些控制类之外的旋转 + */ + override fun rotateToPoint(to: RelativeLocation) { + rotateParticlesToPoint(to.toVector()) + } + + fun rotateParticlesToPoint(to: Vec3) { + change( + {}, mapOf( + PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d(to) + ) + ) + } + + fun changeTick(tick: Int) { + change( + { clientTick = tick }, mapOf( + PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int( + tick + ) + ) + ) + } + + fun changeMaxTick(tick: Int) { + change( + { this.maxTick = tick }, mapOf( + PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(tick) + ) + ) + } + + /** + * 发送行为修改包 + * @param toggleMethod 修改Server层的参数的方法 如果不用此修改方法则会导致其他客户端渲染不同步 + * @param args 此参数出现过的键 必须在otherPacketArgs方法返回的args中存在 否则任然会存在同步问题 + */ + fun change(toggleMethod: ServerParticleGroup.() -> Unit, args: Map>) { + world ?: return + val visible = ServerParticleGroupManager.filterVisiblePlayer(this) + toggleMethod(this) + visible.forEach { + val player = world!!.getPlayerByUUID(it) ?: return@forEach + val packet = PacketParticleGroupS2C(uuid, ControlType.CHANGE, args) + CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) + } + } + + fun initServerGroup(pos: Vec3, world: Level, maxTick: Int = 120) { + this.pos = pos + this.world = world + this.clientMaxTick = maxTick + } + + override fun isValid(): Boolean { + return valid + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt new file mode 100644 index 00000000..1588441b --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt @@ -0,0 +1,200 @@ +package cn.coostack.cooparticlesapi.network.particle + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C.PacketArgsType +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.phys.Vec3 +import java.util.* +import java.util.concurrent.ConcurrentHashMap + + +/** + * 控制所有的ServerParticleGroup + */ +@Deprecated("使用ParticleComposition") + +object ServerParticleGroupManager { + private val serverGroups = ConcurrentHashMap() + + /** + * key playerUUID + * value player can see + */ + internal val visible = ConcurrentHashMap>() + + fun addParticleGroup( + group: ServerParticleGroup, + pos: Vec3, + world: ServerLevel + ) { + serverGroups[group.uuid] = group + group.initServerGroup(pos, world) + world.players().filter { it.position().distanceTo(pos) <= group.visibleRange } + .forEach { receiver -> + addGroupPlayerView(receiver, group) + } + group.onGroupDisplay(pos, world) + } + + fun getParticleGroup(group: UUID): ServerParticleGroup? { + return serverGroups[group] + } + + fun getGroups(): Map { + return Collections.unmodifiableMap(serverGroups) + } + + /** 返回服务端当前旧 ParticleGroup 实例数。 */ + fun groupCount(): Int = serverGroups.size + + fun clearServer() { + serverGroups.onEach { it.value.canceled = true }.clear() + visible.clear() + } + + fun upgrade() { + upgradeGroups() + clearOfflineVisible() + } + + fun filterVisiblePlayer(group: ServerParticleGroup): Set { + val set = HashSet() + visible.forEach { + if (group in it.value) { + set.add(it.key) + } + } + return set + } + + private fun upgradeGroups() { + val iterator = serverGroups.iterator() + while (iterator.hasNext()) { + val value = iterator.next().value + // 更新状态 + if (value.canceled || !value.valid) { + val server = value.world?.server ?: continue + visible.forEach { (t, u) -> + val player = server.playerList.getPlayer(t) ?: return@forEach + if (value in u) { + removeGroupPlayerView(player, value) + u.remove(value) + } + } + iterator.remove() + continue + } + // 更新可见性 + value.world!!.server!!.playerList.players.forEach { p -> + val visibleSet = visible.getOrPut(p.uuid) { HashSet() } + if (p.level().dimension() != value.world?.dimension()) { + // 世界转换 + if (value in visibleSet) { + removeGroupPlayerView(p, value) + } + visibleSet!!.remove(value) + return@forEach + } + if (p.isDeadOrDying) { + if (value in visibleSet) { + removeGroupPlayerView(p, value) + visibleSet!!.remove(value) + } + return@forEach + } + if (value.pos.distanceTo(p.position()) <= value.visibleRange) { + // 防止重复添加(发包) + if (value in visibleSet) { + return@forEach + } + addGroupPlayerView(p, value) + // 同步数据包 + togglePacketView(p, value) + + } else { + if (value in visibleSet) { + removeGroupPlayerView(p, value) + } + // 超过范围 + visibleSet.remove(value) + } + } + value.tick() + } + } + + private fun clearOfflineVisible() { + val server = CooParticlesAPI.serverOrNull ?: return + // 清空所有离线玩家 + val visibleIterator = visible.iterator() + while (visibleIterator.hasNext()) { + val entry = visibleIterator.next() + val player = server.playerList.getPlayer(entry.key) + if (player == null || player.hasDisconnected()) { + visibleIterator.remove() + } + } + } + + private fun togglePacketView(target: ServerPlayer, group: ServerParticleGroup) { + val packet = PacketParticleGroupS2C( + group.uuid, ControlType.CHANGE, + mutableMapOf( + PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(group.pos), + PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(group.axis.toVector()), + PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientTick), + PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientMaxTick), + ) + ) + + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + /** + * 在该玩家的视角中移除这些粒子 + * @param target + * @param targetGroup + */ + private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ServerParticleGroup) { + // 发包给玩家 + val packet = PacketParticleGroupS2C( + targetGroup.uuid, + ControlType.REMOVE, mapOf() + ) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + /* + * @param target + * @param targetGroup + */ + private fun addGroupPlayerView( + target: ServerPlayer, + targetGroup: ServerParticleGroup, + ) { + // 修复一个包发送给一个玩家两次 + val visibleSet = visible.getOrPut(target.uuid) { HashSet() } + visibleSet.add(targetGroup) + // 发包给玩家 + val packet = PacketParticleGroupS2C( + targetGroup.uuid, + ControlType.CREATE, + mutableMapOf( + PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(targetGroup.pos), + PacketArgsType.GROUP_TYPE.ofArgs to ParticleControlerDataBuffers.string(targetGroup.getClientType()!!.name), + PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientTick), + PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientMaxTick), + PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(targetGroup.scale) + ).apply { + putAll(targetGroup.otherPacketArgs()) + } + ) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt new file mode 100644 index 00000000..bf4c43db --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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(): StreamCodec { + return ParticleCompositionRegistryHelper.generateCodec(this) + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt new file mode 100644 index 00000000..013746cd --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt @@ -0,0 +1,16 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +abstract class AutoSequencedParticleComposition(position: Vec3, world: Level? = null) : + SequencedParticleComposition(position, world) { + + override fun getCodec(): StreamCodec { + return ParticleCompositionRegistryHelper.generateCodec(this) + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt new file mode 100644 index 00000000..2b7be814 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt @@ -0,0 +1,51 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.cparticle.CParticle +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.particles.control.ParticleControler +import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect +import java.util.UUID + +class CompositionData : Comparable { + var order = 0 + val uuid = UUID.randomUUID() + val singleParticleHandlers = ArrayList Unit>() + val particleControlerHandlers = ArrayList Unit>() + val cParticleHandlers = ArrayList Unit>() + val cParticleControlerHandlers = ArrayList Unit>() + var displayerBuilder: (UUID) -> ParticleDisplayer = { + ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) + } + private set + + fun setDisplayerSupplier(supplier: (UUID) -> ParticleDisplayer): CompositionData { + displayerBuilder = supplier + return this + } + + fun addParticleInstanceInit(init: ControlableParticle.() -> Unit): CompositionData { + singleParticleHandlers.add(init) + return this + } + + fun addParticleControlerInstanceInit(init: ParticleControler.() -> Unit): CompositionData { + particleControlerHandlers.add(init) + return this + } + + fun addCParticleInstanceInit(init: CParticle.() -> Unit): CompositionData { + cParticleHandlers.add(init) + return this + } + + fun addCParticleControlerInstanceInit(init: CParticleControlable.() -> Unit): CompositionData { + cParticleControlerHandlers.add(init) + return this + } + + override fun compareTo(other: CompositionData): Int { + return order - other.order + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt new file mode 100644 index 00000000..da4a7398 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt @@ -0,0 +1,1177 @@ +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.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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 + +/** + * 因为不想写太多的ControlableBuffer, 而改用Codec自动生成数据 (同时避免写一万个Provider内部类) + * + * 所以重构ParticleStyle + * + * 首先强制 auto update + * + * 使用自动注册 + * @see cn.coostack.cooparticlesapi.annotations.composition.ParticleCompositionRegister + */ +abstract class ParticleComposition : ServerControler, + Controlable, Tickable, NetworkDirtyMarkable { + companion object { + @JvmStatic + fun encodeBase(data: ParticleComposition, buf: FriendlyByteBuf) { + 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: FriendlyByteBuf) { + 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 + + /** + * 如果作为子 composition输入就一定要修改这个 + */ + var controlUUID = UUID.randomUUID() + + var axis = RelativeLocation.yAxis() + + var roll = 0.0 + + val particles = ConcurrentHashMap>() + + /** + * 排除掉 SingleParticleDisplayer 从而减少遍历次数 + */ + val controlerTicks = HashSet>() + + val particleLocations = ConcurrentHashMap, RelativeLocation>() + + val status = CompositionStatusHelper() + + /** 当粒子组合初始化时,存储一倍缩放粒子组与原点的距离。 */ + val particleDefaultLength = ConcurrentHashMap() + + /** 保留一倍缩放时的相对向量,使零缩放后的粒子能够恢复方向。 */ + private val particleDefaultLocations = ConcurrentHashMap() + + // 防止频繁的toList造成的性能浪费 + + 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 + + /** + * 标记此 Composition 的完整网络状态需要重新同步。 + * + * 自定义同步字段优先使用 `var value by dirty(initial)`;普通 `@CodecField` 修改后需要显式调用本方法。 + */ + 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(): StreamCodec + + abstract fun getParticles(): Map + + abstract fun onDisplay() + + /** + * 快捷设置 链式调用 + * + * @param interval + * @return + */ + fun setDisabledInterval(interval: Int): ParticleComposition { + this.status.closedInternal = interval + markNetworkStateDirty() + return this + } + + /** + * 配置此 composition 自动创建的所有 CParticle systems。 + * 名称、容量、渲染层、模式和生命周期由 composition 管理。 + */ + fun configureCParticleSystem(configure: CParticleSystem.() -> Unit): ParticleComposition { + return setCParticleSystemConfiguration(null, configure) + } + + /** 只配置指定渲染层的 system。 */ + 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 + } + + /** 返回此 composition 在指定层自动创建的 system;尚未显示时返回 null。 */ + fun getCParticleSystem(layer: CParticleRenderLayer): CParticleSystem? { + return managedCParticleSystems.firstOrNull { !it.released && it.layer == layer } + } + + /** 返回此 composition 当前持有的 systems 快照。 */ + 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 + } + } + + /** + * 对当前 composition 创建的所有 CParticle systems 播放同一段 GPU 视觉过渡。 + * + * 示例:`playCParticleVisualTransition(20F, scaleCurve = curve)` 会统一缩放当前 systems。 + * 禁止:不要用它修改粒子生成时的基础尺寸。 + * + * @param durationTicks 过渡时长,单位 tick + * @param alphaCurve 不透明度倍率曲线 + * @param scaleCurve 等比缩放倍率曲线 + * @param colorFrom 可选起始颜色 + * @param colorTo 可选结束颜色 + * @param mode 过渡结束后的行为 + * @return 当前 composition + */ + @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, + ) + } + + /** + * 播放 composition 级 GPU 视觉过渡,并允许强制重新开始相同配置。 + * + * 示例:`playCParticleVisualTransition(20F, true, scaleCurve = curve)` 会重置进度。 + * 禁止:不要用它修改粒子生成时的基础尺寸。 + * + * @param durationTicks 过渡时长,单位 tick + * @param restart 是否强制从头播放 + * @param alphaCurve 不透明度倍率曲线 + * @param scaleCurve 等比缩放倍率曲线 + * @param colorFrom 可选起始颜色 + * @param colorTo 可选结束颜色 + * @param mode 过渡结束后的行为 + * @return 当前 composition + */ + @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, + ) + } + + /** + * 把同一组过渡参数分发给当前 composition 管理的 systems。 + * + * 示例:公开重载通过本方法统一传递 [restart]。 + * 禁止:不要在这里缓存已经释放的 system。 + * + * @param durationTicks 过渡时长,单位 tick + * @param alphaCurve 不透明度倍率曲线 + * @param scaleCurve 等比缩放倍率曲线 + * @param colorFrom 可选起始颜色 + * @param colorTo 可选结束颜色 + * @param mode 过渡结束后的行为 + * @param restart 是否强制从头播放 + * @return 当前 composition + */ + 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 + } + + /** 停止当前 composition 的 CParticle GPU 视觉过渡。 */ + @JvmOverloads + fun stopCParticleVisualTransition(reset: Boolean = false): ParticleComposition { + getCParticleSystems().forEach { it.stopVisualTransition(reset) } + return this + } + + /** + * 扫描此 composition 的完整运行时子树,为所有 CParticle systems 播放同一段 GPU alpha 过渡。 + * 子树包括嵌套 composition、ParticleGroupStyle 和 ControlableParticleGroup。 + * 过渡曲线会覆盖粒子实例的 alpha,再与粒子、system 和普通视觉过渡的 alpha 曲线相乘。 + * + * @param durationTicks 过渡时长,单位为 tick,必须为有限正数 + * @param alphaCurve 用于覆盖粒子实例 alpha 的曲线 + * @param mode 过渡结束后的行为 + * @param restart 是否强制替换相同配置 + * @return 当前 composition + * @throws IllegalArgumentException 当 [durationTicks] 不是有限正数时抛出 + */ + @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, + ) + } + + /** 停止此 composition 完整运行时子树中的 CParticle alpha 过渡。 */ + @JvmOverloads + 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() + // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 + // 多为服务端(除非有人使在Client环境创建了这个类) + if (displayed) { + if (gpuTransformActive) { + applyGpuScale(new) + } else { + toggleScaleDisplayed() + } + } + // 发包有效 + if (!canceled) { + // remove过后 无法同步 + 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 + } + + /** + * 清除当前显示内容,并按 [cancel] 决定是否结束本次生命周期。 + * + * 示例:`clear(true)` 会销毁子节点并从客户端活动计数中移除本实例。 + * 禁止用 `clear(false)` 表示最终移除,因为该模式用于保留 displayed 状态后重新生成内容。 + * + * @param cancel `true` 表示结束生命周期,`false` 表示仅刷新显示内容 + */ + 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) + } + } + + /** + * 在服务端重新生成 Composition 前重置旧生命周期状态。 + * + * 示例:同一实例再次调用 `spawn(...)` 时先执行本方法,避免沿用 canceled 状态。 + * 禁止在仍需保留当前客户端显示内容时调用。 + */ + internal fun resetLifecycleForSpawn() { + ParticleCompositionManager.setClientLoaded(this, false) + canceled = false + displayed = false + networkStateDirty = true + networkFullDirty = true + lastNetworkStatus = status.displayStatus + lastNetworkStatusInterval = status.closedInternal + } + + /** + * 根据当前世界进入服务端同步或客户端显示生命周期。 + * + * 示例:客户端直接显示的嵌套 Composition 会在此登记为活动实例。 + * 禁止在 [world] 尚未设置时调用。 + */ + 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) + } + } + + /** 按一倍缩放缓存恢复方向后应用当前缩放,避免从零缩放恢复时产生 NaN。 */ + 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 + } + // 减少一倍的new Vec3 + 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 + // display + 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() + } + + /** + * 显示一个相对坐标对应的粒子节点。 + * + * 示例:普通粒子以 [position] 为原点,启用整组变换的 managed CParticle 先计算实际渲染坐标。 + * 禁止把 system 局部坐标直接当作世界坐标,否则光照和位置相关纹理会从错误位置采样。 + * + * @param data 粒子显示方式和初始化动作 + * @param pos 已完成初始缩放与旋转的相对坐标 + */ + 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) { + data.cParticleControlerHandlers.forEach { handler -> + handler(controler) + } + } + registerCParticleNode(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) + } + + /** + * 为未绑定的 CParticle displayer 取得当前 composition 专用的 system。 + * + * 示例:同一 composition、渲染层和纹理绑定会复用同一个 system。 + * 禁止覆盖调用方已经绑定的 system;这种情况返回 `null` 并保留原有行为。 + * + * @param displayer 待绑定的 GPU 粒子显示器 + * @return 本次绑定的 managed system;无法绑定或已有外部绑定时返回 `null` + */ + 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 + } + + /** + * 计算粒子显示与环境采样使用的世界坐标。 + * + * 示例:整组变换启用后,序列后续粒子使用 system 当前矩阵换算真实渲染位置。 + * 禁止在这里返回槽位局部坐标;[CParticleSystem] 会在写入时自行逆变换。 + * + * @param pos composition 中保存的相对坐标 + * @param managedSystem 当前 composition 管理的 GPU system,普通显示器传 `null` + * @return 写入显示器的世界坐标 + */ + 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(), + ) + } + + /** + * 返回整组 GPU 变换启用时使用的稳定槽位坐标。 + * + * 示例:system 原点为 `(10, 20, 30)` 时,相对坐标 `(2, 3, 4)` 写为 `(12, 23, 34)`。 + * 禁止在 CPU 路径提供该坐标;此时 system 需要按现存矩阵逆变换真实世界坐标。 + * + * @param pos composition 保存的相对坐标 + * @param managedSystem 当前 composition 管理的 GPU system + * @return 槽位写入坐标;不使用整组 GPU 变换时返回 `null` + */ + 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: Double) { + val axisVector = normalizedAxis(rotationAxis) + val delta = Matrix4f().rotate(radian.toFloat(), 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt new file mode 100644 index 00000000..38ba6dbc --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt @@ -0,0 +1,156 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.StatusHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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 + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + + /** + * 当反转到0时会自动清理粒子 + */ + 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 + } + + /** + * 改用apply是因为 要防止和addPreTickAction冲突 + * + * @return + */ + 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(): StreamCodec { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt new file mode 100644 index 00000000..6ee8e42c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt @@ -0,0 +1,518 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.api.controler.Tickable +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.helper.SequencedCompositionAnimationHelper +import cn.coostack.cooparticlesapi.utils.storage.Memo +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.UUID +import kotlin.math.max +import kotlin.math.min + +/** + * 自动更新(AutoToggle) 生长动画 + * + * AI神力 根本懒得再写一份Sequenced的版本了 + */ +abstract 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) + + companion object { + @JvmStatic + fun encodeBase(data: SequencedParticleComposition, buf: FriendlyByteBuf) { + ParticleComposition.encodeBase(data, buf) + buf.writeInt(data.count) + buf.writeInt(data.displayedParticleCount) + buf.writeInt(data.serverCurrentIndex) + buf.writeLongArray(data.index.get()) + } + + @JvmStatic + fun decodeBase(instance: SequencedParticleComposition, buf: FriendlyByteBuf) { + ParticleComposition.decodeBase(instance, buf) + instance.count = buf.readInt() + instance.displayedParticleCount = buf.readInt() + instance.serverCurrentIndex = buf.readInt() + instance.index.setMemoValue(buf.readLongArray()) + } + } + + /** + * 这里采用 animate框架 写多个条件用来方便生长动画的条件变化 + * 在init 或者 onDisplay执行 + */ + val animate = + SequencedCompositionAnimationHelper() + .loadComposition(this) + + + var count = 0 + var index = Memo { + var page = count / 64 + if (count % 64 > 0) { + page++ + } + LongArray(page) + } + + /** 服务端:已经“显示”的粒子数量(仅逻辑字段,可用于业务) */ + var displayedParticleCount: Int = 0 + protected set + + /** 服务端:下一个将要生成的 index 指针(用于 addSingle/addMultiple 生长动画) */ + var serverCurrentIndex: Int = 0 + protected set + protected val sequencedParticlesData = ArrayList>() + override fun getParticles(): SortedMap { + return getParticleSequenced() + } + + /** + * 不要调用只有客户端才能调用的方法 因为服务器也会调用一次这个方法用来获取实际的粒子个数 + * + * @return 粒子相对样式 + */ + abstract fun getParticleSequenced(): SortedMap + final override fun tick() { + super.tick() + } + + override fun flush() { + if (particles.isNotEmpty() || indexToUuid.any { it != null }) { + clear(false) + } + displayParticles() + restoreDisplayedParticles() + } + + override fun clear(cancel: Boolean) { + super.clear(cancel) + sequencedParticlesData.clear() + particleRotatedLocations.clear() + indexToUuid.fill(null) + } + + /** + * 进入序列 Composition 的显示生命周期并初始化动画状态。 + * + * 示例:客户端直接显示时会登记为一个活动 Composition 实例。 + * 禁止在 [world] 尚未设置时调用。 + */ + override fun display() { + if (displayed) { + return + } + displayed = true + // 修复禁用后不自动移除的问题 + status.loadControler(this) + status.initHelper() + this.client = world!!.isClientSide + if (client) { + ParticleCompositionManager.setClientLoaded(this, true) + } + // 在服务器需要用来更新粒子个数 所以需要参与一次计算 + flush() + if (!client) { + // 服务器只负责数据同步,不负责粒子生成。 + onDisplay() + return + } + onDisplay() + } + + open fun beforeDisplaySequenced(map: SortedMap) { + + } + + /** + * 这里非常关键: + * - 只计算序列数据、count、缩放/旋转后的相对坐标 + * - 不创建粒子对象(不调用 displayEntry) + */ + override fun displayParticles() { + val locations = getParticles() + val newCount = locations.size + prepareGpuComposition(newCount) + + // 更新粒子总数并扩容位集,同时保留已有状态。 + ensureIndexCapacity(newCount) + + count = newCount + + // 服务端不做任何粒子生成 + if (!client) { + return + } + + beforeDisplaySequenced(locations) + Math3DUtil.rotatePointsToPoint(locations.values.toList(), axis, RelativeLocation.yAxis()) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, roll) + toggleScale(locations) + + sequencedParticlesData.clear() + sequencedParticlesData.addAll(locations.toList()) + particleRotatedLocations.clear() + particleRotatedLocations.addAll(locations.values) + // 客户端:准备索引到 UUID 的映射数组。 + if (indexToUuid.size != count) { + indexToUuid = arrayOfNulls(count) + } + } + + override fun update(other: ParticleComposition) { + super.update(other) + other as SequencedParticleComposition + val oldIndex = this.index.get().copyOf() + val oldCount = this.count + + this.count = other.count + this.displayedParticleCount = other.displayedParticleCount + this.serverCurrentIndex = other.serverCurrentIndex + + this.index.setMemoValue(other.index.get()) + + if (!client) return + + // 粒子总数或缓存结构变化时,客户端需要重新计算序列数据。 + if (oldCount != this.count || sequencedParticlesData.size != this.count) { + // 缓存重建会移除旧节点,flush 会按新的完整位集恢复已显示状态。 + flush() + return + } + + applyIndexDiff(oldIndex, this.index.get()) + restoreDisplayedParticles() + } + + private fun restoreDisplayedParticles() { + if (!client || sequencedParticlesData.size != count) return + for (i in 0 until count) { + if (isParticleDisplayed(i)) createWithIndex(i) + } + } + + private fun applyIndexDiff(oldBits: LongArray, newBits: LongArray) { + + if (!client) return + val n = count + if (n <= 0) return + if (sequencedParticlesData.size != n) return + + val pages = pagesFor(n) + // 复制到同长度,避免越界判断 + val oldSafe = if (oldBits.size == pages) oldBits else oldBits.copyOf(pages) + val newSafe = if (newBits.size == pages) newBits else newBits.copyOf(pages) + + for (page in 0 until pages) { + var diff = oldSafe[page] xor newSafe[page] + if (diff == 0L) continue + + val base = page shl 6 // 当前页的首个槽位 + while (diff != 0L) { + val bit = diff.countTrailingZeroBits() // 0..63 + val i = base + bit + if (i >= n) break + + val newGen = ((newSafe[page] ushr bit) and 1L) != 0L + if (newGen) createWithIndex(i) else removeWithIndex(i) + + diff = diff and (diff - 1) // 清除最低位的 1 + } + } + } + + + /** + * 服务端:生成下一个粒子(只改 位集 和计数,不生成粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * addSingle() + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断。 + * 方法从当前指针向后寻找第一个未显示槽位,必要时回绕;成功后会标记完整网络状态。 + * 服务端调用必须位于 Composition 生命周期更新所在的逻辑线程。 + */ + fun addSingle() { + if (count <= 0) return + val start = serverCurrentIndex.coerceIn(0, count - 1) + val idx = (start until count).firstOrNull { !isParticleDisplayed(it) } + ?: (0 until start).firstOrNull { !isParticleDisplayed(it) } + ?: return + + setBit(index.get(), idx, true) + displayedParticleCount++ + + // 客户端:立即生成该索引对应的粒子。 + if (client) { + createWithIndex(idx) + } + serverCurrentIndex = min(idx + 1, max(count - 1, 0)) + markDirty() + } + + /** + * 服务端:生成多个粒子(只改 位集 和计数,不生成粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * addMultiple(amount) + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * @param amount 生成数量(小于等于 0 时直接返回) + */ + fun addMultiple(amount: Int) { + if (amount <= 0) return + repeat(amount) { addSingle() } + } + + /** + * 服务端:移除上一个粒子(只改 位集 和计数,不删除粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * removeSingle() + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * 说明: + * - 从 `serverCurrentIndex` 向前寻找最近的已显示槽位,必要时回绕 + * - 成功后清除位、更新计数和指针,并标记完整网络状态 + * - 客户端场景会立即调用 `removeWithIndex`,服务端调用必须位于 Composition 生命周期更新线程 + */ + fun removeSingle() { + if (count <= 0) return + val start = serverCurrentIndex.coerceIn(0, count - 1) + val idx = (start downTo 0).firstOrNull(::isParticleDisplayed) + ?: (count - 1 downTo start + 1).firstOrNull(::isParticleDisplayed) + ?: return + + setBit(index.get(), idx, false) + displayedParticleCount-- + + // 客户端:立即删除该索引对应的粒子。 + if (client) { + removeWithIndex(idx) + } + serverCurrentIndex = max(idx - 1, 0) + markDirty() + } + + /** + * 服务端:移除多个粒子(只改 位集 和计数,不删除粒子对象) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * removeMultiple(amount) + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * @param amount 移除数量(小于等于 0 时直接返回) + */ + fun removeMultiple(amount: Int) { + if (amount <= 0) return + repeat(amount) { removeSingle() } + } + + /** + * 重置所有粒子状态(清空 位集、计数器、指针) + * + * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 + * 一定要在非客户端作用域执行 + * ```kotlin + * if (!client){ + * resetAll() + * } + * ``` + * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 + * + * 说明: + * - 服务端:只清位集、计数和指针,等待同步到客户端后按差异处理 + * - 客户端:先遍历位集,把已生成的粒子对象全部通过 `removeWithIndex(i)` 移除,再清空位集 + * + * 注意: + * - resetAll() 会把 serverCurrentIndex 重置为 0 + * - displayedParticleCount 会重置为 0 + */ + fun resetAll() { + // 客户端:先删除所有已经生成的粒子对象。 + if (client && count > 0) { + val pages = pagesFor(count) + val bits = index.get() + for (page in 0 until min(bits.size, pages)) { + var v = bits[page] + if (v == 0L) continue + val base = page shl 6 + while (v != 0L) { + val bit = v.countTrailingZeroBits() + val i = base + bit + if (i >= count) break + removeWithIndex(i) + v = v and (v - 1) + } + } + } + + // 清空位集。 + val arr = index.get() + val changed = displayedParticleCount != 0 || serverCurrentIndex != 0 || arr.any { it != 0L } + for (i in arr.indices) arr[i] = 0L + + displayedParticleCount = 0 + serverCurrentIndex = 0 + if (changed) markDirty() + } + + override fun rotateToPoint(to: RelativeLocation) { + super.rotateToPoint(to) + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + super.rotateToWithAngle(to, radian) + } + + override fun rotateAsAxis(radian: Double) { + super.rotateAsAxis(radian) + } + + /** + * 直接设置一个序列槽位的显示状态。 + * + * 服务端真实变更后会标记完整网络状态,客户端真实变更后会立即创建或移除对应节点。 + * 越界或状态未变化时不会修改计数、节点和网络脏状态。 + * + * @param index 目标槽位索引,有效范围为 `0 until count` + * @param generated `true` 表示显示,`false` 表示移除 + */ + fun setParticleStatus(index: Int, generated: Boolean) { + if (index !in 0 until count || isParticleDisplayed(index) == generated) return + setBit(this.index.get(), index, generated) + displayedParticleCount += if (generated) 1 else -1 + if (client) { + if (generated) createWithIndex(index) else removeWithIndex(index) + } + markDirty() + } + + fun isParticleDisplayed(index: Int): Boolean { + if (index !in 0 until count) return false + return getBit(this.index.get(), index) + } + + final override fun toggleScaleDisplayed() { + if (!displayed) { + return + } + sequencedParticlesData.forEach { (data, location) -> + applyScale(data.uuid, location) + } + toggleRelative() + } + + /** + * 客户端使用的索引到 UUID 映射,用于按索引删除。 + * 服务器不会读取该映射。 + */ + private var indexToUuid: Array = emptyArray() + private fun createWithIndex(i: Int) { + if (!client) return + if (i !in 0 until sequencedParticlesData.size) return + if (indexToUuid.size != count) indexToUuid = arrayOfNulls(count) + + // 如果已经创建过就跳过(避免重复) + if (indexToUuid[i] != null) return + + val (data, rl) = sequencedParticlesData[i] + displayEntry(data, rl) + if (particles.containsKey(data.uuid)) { + indexToUuid[i] = data.uuid + } + } + + override fun displayEntry(data: CompositionData, pos: RelativeLocation) { + super.displayEntry(data, pos) + refreshGpuTransformMode() + } + + override fun trackDisplayedParticleLocation(pos: RelativeLocation) = Unit + + + private fun removeWithIndex(i: Int) { + if (!client) return + if (i !in 0 until count) return + if (indexToUuid.size != count) return + + val uuid = indexToUuid[i] ?: return + val obj = particles[uuid] ?: run { + indexToUuid[i] = null + return + } + + unregisterCParticleNode(obj) + obj.remove() + if (obj is Tickable<*>) { + controlerTicks.remove(obj) + } + particles.remove(uuid) + particleLocations.remove(obj) + indexToUuid[i] = null + refreshGpuTransformMode() + } + + + private fun ensureIndexCapacity(newCount: Int) { + val pages = pagesFor(newCount) + val current = index.get() + if (current.size == pages) return + + val resized = LongArray(pages) + // 把旧页复制到新数组 + val len = min(current.size, resized.size) + for (i in 0 until len) resized[i] = current[i] + index.setMemoValue(resized) + } + + private fun pagesFor(cnt: Int): Int { + if (cnt <= 0) return 0 + return (cnt + 63) / 64 + } + + private fun getBit(arr: LongArray, index: Int): Boolean { + if (arr.isEmpty()) return false + val page = index ushr 6 // /64 + if (page !in arr.indices) return false + val bit = index and 63 + return ((arr[page] ushr bit) and 1L) == 1L + } + + private fun setBit(arr: LongArray, index: Int, value: Boolean) { + if (arr.isEmpty()) return + val page = index ushr 6 + if (page !in arr.indices) return + val bit = index and 63 + val mask = 1L shl bit + arr[page] = if (value) (arr[page] or mask) else (arr[page] and mask.inv()) + } + +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt new file mode 100644 index 00000000..69a2c31a --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt @@ -0,0 +1,203 @@ +package cn.coostack.cooparticlesapi.network.particle.composition + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.StatusHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.TreeMap +import java.util.UUID + +/** + * - 只用于客户端嵌套(不允许单独发包生成) + * - 继承 SequencedParticleComposition:提供 SortedMap 序列数据(按 CompositionData.order 排序) + */ +class SequencedParticleShapeComposition(uuid: UUID) : SequencedParticleComposition(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 + + var spawnAge = 0 + + /** + * 是否在reversed 到0的时候清理粒子 (防止残留影响观感) + */ + var reversedClean = true + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + private set + + fun loadScaleHelper(min: Double, max: Double, scalingTick: Int): SequencedParticleShapeComposition { + scaleHelper = CompositionScaleHelper(min, max, scalingTick).apply { + loadControler(this@SequencedParticleShapeComposition) + } + scalePreTick = true + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): SequencedParticleShapeComposition { + scaleHelper = CompositionBezierScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + /** + * 改用apply是因为 要防止和addPreTickAction冲突 + */ + fun applyDisplayAction(action: SequencedParticleShapeComposition.() -> Unit): SequencedParticleShapeComposition { + invokes.add(action) + return this + } + + fun applyBeforeDisplayAction( + action: SequencedParticleShapeComposition.(SortedMap) -> Unit + ): SequencedParticleShapeComposition { + beforeInvokes.add(action) + return this + } + + fun applyPoint( + point: RelativeLocation, + dataSupplier: (RelativeLocation, Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to dataSupplier) + return this + } + + fun applyPointRel( + point: RelativeLocation, + dataSupplier: (RelativeLocation) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(rel) }) + return this + } + + fun applyPointI( + point: RelativeLocation, + dataSupplier: (Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(o) }) + return this + } + + + fun applyBuilder( + builder: PointsBuilder, + dataSupplier: (RelativeLocation, Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(builder to dataSupplier) + return this + } + + fun applyBuilderI( + builder: PointsBuilder, + dataSupplier: (Int) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(builder to { rel, o -> dataSupplier(o) }) + return this + } + + fun applyBuilderRel( + builder: PointsBuilder, + dataSupplier: (RelativeLocation) -> CompositionData + ): SequencedParticleShapeComposition { + points.add(builder to { rel, o -> dataSupplier(rel) }) + return this + } + + fun setReversedScaleOnDisableStatus(status: StatusHelper): SequencedParticleShapeComposition { + addPreTickAction { + if (status.displayStatus == 2) { + scaleReversed = true + } + } + return this + } + + fun setReversedScaleOnCompositionStatus(composition: ParticleComposition): SequencedParticleShapeComposition { + addPreTickAction { + if (composition.status.getCurrentStatus() == StatusHelper.Status.DISABLE) { + scaleReversed = true + } + } + return this + } + + override fun getCodec(): StreamCodec { + throw NotImplementedError("此类只作为客户端嵌套使用, 不能单独生成! ") + } + + /** + * SequencedParticleComposition 需要返回 SortedMap + * TreeMap 会按 CompositionData.compareTo -> order 排序 + */ + override fun getParticleSequenced(): SortedMap { + val res: SortedMap = TreeMap() + var order = 0 + points.forEach { (builder, supplier) -> + res.putAll( + builder.createWithCompositionData { rel -> + supplier(rel, order++) + } + ) + } + return res + } + + + override fun beforeDisplaySequenced(map: SortedMap) { + super.beforeDisplaySequenced(map) + beforeInvokes.forEach { + it(map) + } + } + + override fun onDisplay() { + animate.clientOnly() + 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt new file mode 100644 index 00000000..bafbcb49 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt @@ -0,0 +1,379 @@ +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.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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 { + /** + * 弱引用保存已进入客户端显示生命周期的 Composition 实例。 + * + * 示例:顶层 Composition 及其直接显示的子 Composition 会分别占一个条目。 + * 禁止在此处强持有 Composition,否则 Emitter 临时创建的实例无法被回收。 + */ + private val loadedClientCompositions = WeakIdentitySet() + + val clientView = ConcurrentHashMap() + + val serverView = ConcurrentHashMap() + + /** + * 只有玩家可见才会处理发包 + */ + val playerPlayerVisibleSet = ConcurrentHashMap>() + + val registeredTypes = ConcurrentHashMap>() + + /** + * 登记或移除一个客户端活动 Composition。 + * + * 示例:Composition 首次 `display()` 时传入 `true`,执行 `clear(true)` 时传入 `false`。 + * 禁止为服务端仅用于同步的 Composition 传入 `true`。 + * + * @param composition 需要更新活动状态的 Composition 实例 + * @param loaded `true` 表示已加载到客户端,`false` 表示已离开客户端生命周期 + */ + internal fun setClientLoaded(composition: ParticleComposition, loaded: Boolean) { + if (loaded) { + loadedClientCompositions.add(composition) + } else { + loadedClientCompositions.remove(composition) + } + } + + /** + * 返回客户端当前活动的全部 Composition 实例数量。 + * + * 示例:F3 调试信息用该值统计顶层、嵌套及 Emitter 直接显示的 Composition。 + * 禁止把该值理解为服务端实例数或已注册类型数。 + * + * @return 当前客户端活动 Composition 数量 + */ + @JvmStatic + fun loadedClientCount(): Int = loadedClientCompositions.size() + + /** 返回当前客户端活动的全部 Composition 快照,包含嵌套实例。 */ + internal fun debugCompositions(): List = loadedClientCompositions.snapshot() + + /** 返回服务端当前活动 Composition 实例数。 */ + 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 = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) + 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) + } + } + } + + /** + * 清理当前客户端世界持有的所有 Composition。 + * + * 示例:客户端断线或换世界时调用该方法,将顶层与嵌套实例一并移出活动计数。 + * 禁止用该方法清理服务端 [serverView]。 + */ + fun clearClient() { + // 这里必须走 clear(true) 强制销毁 + // remove() 可能被使用者重写成延迟消散的语义 (例如先 status.disable() 等渐隐结束再真正销毁) + // 客户端断连/换世界要求立刻干净 否则 composition 会脱离 clientView 变成孤儿粒子 + clientView.values.forEach { + it.clear(true) + } + clientView.clear() + loadedClientCompositions.clear() + } + + + fun clearServer() { + // 同 clearClient 不能依赖使用者重写的 remove() + 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt new file mode 100644 index 00000000..20e2b631 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt @@ -0,0 +1,26 @@ +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) +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt new file mode 100644 index 00000000..acb79b08 --- /dev/null +++ b/neoforge/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) +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt new file mode 100644 index 00000000..2fb0b504 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt @@ -0,0 +1,24 @@ +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) +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt new file mode 100644 index 00000000..e342b4b6 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.network.particle.data + +abstract class RangeData>(var min: T, var max: T) { + init { + require(min <= max) { "min must be <= $max" } + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt new file mode 100644 index 00000000..2c83b326 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt @@ -0,0 +1,29 @@ +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.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * 自动生成网络 codec 并以实现类完整类名注册的 [ClassEmitters]。 + * + * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; + * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, + * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 + * + * @param pos 发射器生成位置 + * @param world 发射器生效世界 + */ +abstract class AutoEmitters(pos: Vec3, world: Level?) : ClassEmitters(pos, world) { + override fun getEmittersID(): String { + return this::class.java.name + } + + override fun getCodec(): StreamCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt new file mode 100644 index 00000000..2491381d --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt @@ -0,0 +1,32 @@ +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.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * 自动生成网络 codec 并以实现类完整类名注册的 [ClassParticleEmitters]。 + * + * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; + * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, + * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 + * + * @see ParticleEmittersRegistryHelper + * @constructor 实现类必须提供空构造方法或 `(Vec3, Level?)` 构造方法 + * + * @param pos 发射器生成位置 + * @param world 发射器生效世界 + */ +abstract class AutoParticleEmitters(pos: Vec3, world: Level?) : ClassParticleEmitters(pos, world) { + override fun getEmittersID(): String { + return this::class.java.name + } + + override fun getCodec(): StreamCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt new file mode 100644 index 00000000..b0f80bb3 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt @@ -0,0 +1,29 @@ +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.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * 自动生成网络 codec 并以实现类完整类名注册的 [TransformableCParticleEmitter]。 + * + * 实现类必须标注 [CooAutoRegister],并提供公开的 `(Vec3, Level?)` 构造器。 + * 普通同步字段可以使用 [CodecField];会在运行时修改的 Kotlin 属性建议使用 + * `var value by dirty(initialValue)`,赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 + * 基类生命周期和变换字段会自动编码。 + * + * @param pos 发射器世界坐标 + * @param world 发射器所在世界 + */ +abstract class AutoTransformableCParticleEmitter(pos: Vec3, world: Level?) : + TransformableCParticleEmitter(pos, world) { + final override fun getEmittersID(): String = this::class.java.name + + final override fun getCodec(): StreamCodec { + return ParticleEmittersRegistryHelper.generateCodec(this) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt new file mode 100644 index 00000000..01992fcc --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +/** + * 定义 [TransformableCParticleEmitter] 新生成粒子使用的坐标空间。 + * + * [LOCAL] 把粒子位置和速度保存在发射器局部空间中。粒子生成后仍会跟随发射器平移、 + * [TransformableCParticleEmitter.emitterRotation] 和缩放, + * 也是默认模式。[WORLD] 使用普通 GPU 发射器的世界坐标语义,已经生成的粒子不再受发射器变换影响。 + * 模式切换只影响后续生成的粒子,已有粒子继续留在各自创建时的空间中。 + * + * @property networkId 稳定的网络编号,不依赖枚举声明顺序 + */ +enum class CParticleEmitterSpace(val networkId: Int) { + /** 粒子属于发射器局部空间,并持续跟随发射器变换。 */ + LOCAL(0), + + /** 粒子生成后保留在世界空间,不跟随发射器变换。 */ + WORLD(1); + + companion object { + /** + * 按稳定网络编号恢复发射空间。 + * + * @param networkId 网络数据中的空间编号 + * @return 对应的发射空间 + * @throws IllegalArgumentException 编号未知时抛出 + */ + @JvmStatic + fun fromNetworkId(networkId: Int): CParticleEmitterSpace { + return entries.firstOrNull { it.networkId == networkId } + ?: throw IllegalArgumentException("unknown CParticle emitter space: $networkId") + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt new file mode 100644 index 00000000..b018ccde --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt @@ -0,0 +1,411 @@ +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.FriendlyByteBuf +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 + +/** + * 通用发射器实现,兼容多种可控对象: + * - [ControlableParticleData] + * - [ParticleComposition] + * - [DisplayEntity] + */ +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>() + + /** + * 是否启用线段插值器。 + * + * 如果设置为 `true`,每次发射会沿插值结果逐点生成;设置为 `false` 时,每 tick 只执行一次生成。 + */ + var enableInterpolator = false + + /** + * 发射器位移插值器。 + * + * 仅在 [enableInterpolator] 为 `true` 时生效,可通过 `setRefiner` 调整细分密度。 + */ + 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: FriendlyByteBuf) { + 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) + } + + /** 写法:先在 codec 的 decode 中创建对象,再把 `buf` 和 `container` 传入此方法,最后继续解码子类字段。 */ + fun decodeBase(container: ClassEmitters, buf: FriendlyByteBuf) { + 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) + } + + /** 质量,单位 g。 */ + 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 + } + + /** + * 服务器和客户端都会执行此方法。 + * + * 如需仅在服务端执行,请自行判断: + * ```kotlin + * if (!world!!.isClientSide) { ... } + * ``` + */ + abstract fun doTick() + + /** + * 可控对象生成器。 + * + * @param lerpProgress 发射器位移插值器的插值进度 + */ + abstract fun genControls(lerpProgress: Float): List> + + /** + * 在一次生成前会执行。 + * + * @param current 当前插值点的位置 + * @param lerpProgress 当前生成进度 + */ + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + /** + * 通用可控对象的生成后回调。 + * + * @param data 当前生成对象的数据 + * @param spawnPos 生成位置(对象创建后再改该值无效) + * @param particleLerpProgress 当前对象在本次批量生成中的进度 + * @param posLerpProgress 发射器位移插值进度(未启用插值时恒为 1) + */ + abstract fun singleControlableAction( + controler: Controlable<*>, + data: SerializableData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + /** + * 处理单个 controler 的移动方案 + */ + protected open fun moveSingleControler( + controler: Controlable<*>, + data: SerializableData, + to: Vec3, + collide: BlockHitResult + ) { + controler.teleportTo(to) + } + + /** + * 读取当前 controler 世界坐标。 + * + * 子类可重写以支持自定义可控对象,默认支持: + * - [ControlableParticle] + * - [ParticleComposition] + * - [DisplayEntity] + */ + 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 + } + } + + /** + * 读取当前 controler 的速度向量。 + * + * 子类可重写以支持自定义数据类型;默认对 [ControlableParticleData] 生效。 + */ + 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) + } + } + + + /** + * 数据同步需要实现此方法。 + * + * @param emitters 需要同步到当前实例的发射器模板 + */ + 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt new file mode 100644 index 00000000..a9fe79a5 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt @@ -0,0 +1,594 @@ +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.FriendlyByteBuf +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>() + + /** + * 是否启用线段插值器 如果设置为true 则spawnParticles由线段插值器管控 设置为false,则只会在 + * 1tick内执行一次spawnParticles + */ + var enableInterpolator = false + + /** + * 插值器工具 需要启用 enableInterpolator 可以修改 + * + * @see enableInterpolator + */ + var emittersInterpolator: Interpolator = LineEmitterInterpolator() + .setRefiner(5.0) + + override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + val handlerID = handler.getHandlerID() + // 自动注册不适用于多人 + // TODO + 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 { + // 保留innerClass为false的 + it.value.filter { it -> + !it.value + }.keys + } + } + + + companion object { + fun encodeBase(data: ClassParticleEmitters, buf: FriendlyByteBuf) { + 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) + } + + /** 写法 先在codec的 decode方法中 创建此对象 然后将buf和container 传入此方法 然后继续decode自己的参数 */ + fun decodeBase(container: ClassParticleEmitters, buf: FriendlyByteBuf) { + 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) + } + + /** + * 旧版 GPU 力场列表接口。 + * + * 旧 emitter 可以继续覆写此方法;桥接层会在每 tick 调用 + * [submitCParticleForces],由默认实现把列表内容提交到 sink。 + * 新 emitter 应直接覆写流式接口,避免创建临时列表。 + * + * @return 按执行顺序排列的力场列表 + */ + open fun cparticleForces(): List = emptyList() + + /** + * GPU 模式附加力场 (每 tick 同步一次)。 + * + * 默认实现兼容旧版 [cparticleForces];新 emitter 可以直接向 sink 提交, + * 从而避免每 tick 构建临时列表。内置 ParticleCommand 可用 + * [CParticleForce.fromCommand] 转换。 + * + * @param sink 当前 emitter 的力场快照,调用期间只应提交本 emitter 的力 + */ + open fun submitCParticleForces(sink: CParticleForceSink) { + sink.submitAll(cparticleForces()) + } + + /** + * CParticle 方块碰撞相对 system 原点的最大保证范围,单位为方块。 + * + * 示例:粒子最远会离开 system 原点 40 格时返回 `40`。 + * 禁止:返回值会被限制在 `0..96`;范围越大,网格刷新成本按立方增长。 + */ + open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE + + /** 质量 单位 g */ + 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, + ) + } + } + + /** + * 服务器和客户端都会执行此方法 判断服务器清使用 + * + * ```kotlin + * if(!world!!.isClient) + * ``` + */ + abstract fun doTick() + + /** + * 粒子样式生成器 + * @param lerpProgress 粒子发射器位移插值器的插值进度 + * */ + abstract fun genParticles(lerpProgress: Float): List> + + /** + * 在一次粒子生成前会执行 + * + * @param current 当前插值的生成位置 + * @param lerpProgress 粒子生成的进度 + */ + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + /** + * 如若要修改粒子的位置, 速度 属性 请直接修改 ControlableParticleData + * + * @param data 用于操作单个粒子属性的类 + * @param spawnPos 生成的位置,在粒子已经生成后再修改无效 + * 执行tick方法请使用controler.addPreTickAction + * @param particleLerpProgress + * 生成这个粒子的时候,当前的进度((当前生成的粒子索引+1)/genParticles(lerpProgress: Float).size ) + * @param posLerpProgress 当发射器进行发射插值时, 插值的偏移 如果不使用插值则永远为1 + */ + abstract fun singleParticleAction( + controler: ParticleControler, + data: ControlableParticleData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) + + /** + * 当粒子死亡(age >= maxAge然后remove)时 会执行这个方法 + * + * @param oldControler + * @param oldData + * @param respawnCount 重新生成的次数 (0代表第一次从emitter生成) + * @param reason 粒子移除原因 + * @return 会根据data生成新粒子 第二个pair是和粒子死亡位置的相对位置(不是发射器相对位置) + */ + 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 + } + // cparticle GPU 路径: 数据直接进 GPU 粒子系统, 跳过 controler/事件/碰撞 + 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 { + // 针对 ParticleHitEntityEvent + 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 { + // 针对 ParticleOnLiquidEvent + 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 + // 判断现在的位置是不是液体 + if (!world.shouldTickBlocksAt(blockPos)) { + return@addPreTickAction + } + val state = world.getBlockState(blockPos) + val currentLiquid = !state.isSolid + control.bufferedData["cross_liquid"] = currentLiquid + if (beforeLiquid || !currentLiquid) { + return@addPreTickAction + } + // 前一个tick不是液体 当前tick是液体则触发事件 + 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) { + // 找方向 velocity + handlerList[ParticleOnGroundEvent.EVENT_ID]?.let { + val offset = clipRes.direction.normal.asVec3() * 0.1 + val event = ParticleOnGroundEvent( + this, + data, + ofFloored(prepareMove), + clipRes.location.add(offset), + clipRes + ) + for ((handler, _) in it) { + if (handler.getTargetEventID() != ParticleOnGroundEvent.EVENT_ID) { + continue + } + handler.handle(event) + if (event.canceled) { + break + } + } + } + } + + if (clipRes.type != HitResult.Type.MISS) { + handlerList[ParticleCollideEvent.EVENT_ID]?.let { + val event = ParticleCollideEvent( + this, data, clipRes + ) + for ((handler, _) in it) { + 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) + } + + + /** + * # 处理单个粒子的位移位置 + * - 方便更真实的物理模拟 + * - 用于修改particle受到velocity时的移动 + * + * @param particle 被操作位置的粒子 + * @param data 被操作位置的粒子数据 + * @param to 正常情况下粒子应该移动到的位置(单纯loc+velocity) + * @param collide 粒子的碰撞情况 (如果显示碰撞,则代表to位置存在方块 loc不存在) + */ + protected open fun moveSingleParticleWithVelocity( + particle: ControlableParticle, + data: ControlableParticleData, + to: Vec3, + collide: BlockHitResult + ) { + particle.teleportTo(to) + } + + /** + * 数据同步需要实现此方法 + * + * @param emitters 更新的模板发射器 + */ + 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt new file mode 100644 index 00000000..577f7b1e --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt @@ -0,0 +1,117 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.SerializableData +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import io.netty.buffer.Unpooled +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * SerializableData wrapper for ParticleComposition emitters entry. + */ +class CompositionEmittersData( + var compositionType: String = "", + var compositionData: ByteArray = ByteArray(0) +) : SerializableData { + constructor(composition: ParticleComposition) : this() { + setComposition(composition) + } + + companion object { + @JvmStatic + val PACKET_CODEC: StreamCodec = + StreamCodec.of( + { buf, data -> + buf.writeUtf(data.compositionType) + buf.writeInt(data.compositionData.size) + buf.writeBytes(data.compositionData) + }, + { buf -> + val type = buf.readUtf() + val size = buf.readInt() + val bytes = ByteArray(size) + buf.readBytes(bytes) + CompositionEmittersData(type, bytes) + } + ) + + @JvmStatic + fun fromComposition(composition: ParticleComposition): CompositionEmittersData { + val buf = FriendlyByteBuf(Unpooled.buffer()) + composition.getCodec().encode(buf, composition) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + return CompositionEmittersData(composition::class.java.name, data) + } + } + + private var prepared: ParticleComposition? = null + + private fun decodeComposition(): ParticleComposition { + val codec = ParticleCompositionManager.registeredTypes[compositionType] + ?: throw IllegalStateException("ParticleComposition codec not registered for type: $compositionType") + return codec.decode(FriendlyByteBuf(Unpooled.wrappedBuffer(compositionData.copyOf()))) + } + + private fun cloneComposition(composition: ParticleComposition): ParticleComposition { + val type = composition::class.java + val ins = runCatching { + type.getDeclaredConstructor(Vec3::class.java, Level::class.java) + .apply { isAccessible = true } + .newInstance(composition.position, composition.world) + }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() + + return ins.apply { update(composition) } + } + + private fun resolveComposition(): ParticleComposition { + return prepared ?: decodeComposition().also { prepared = it } + } + + override fun getCodec(): StreamCodec { + return PACKET_CODEC + } + + override fun clone(): SerializableData { + val composition = runCatching { cloneComposition(resolveComposition()) }.getOrNull() + return if (composition != null) { + CompositionEmittersData(composition) + } else { + CompositionEmittersData(compositionType, compositionData.copyOf()) + } + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + val composition = resolveComposition() + composition.world = world + composition.setPositionWithoutToggle(pos) + prepared = composition + return composition + } + + override fun getDisplayer(): ParticleDisplayer { + val composition = resolveComposition() + return ParticleDisplayer.withComposition(composition) + } + + fun setComposition(composition: ParticleComposition): CompositionEmittersData { + val clone = cloneComposition(composition) + val data = fromComposition(clone) + compositionType = data.compositionType + compositionData = data.compositionData + prepared = clone + return this + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt new file mode 100644 index 00000000..4d04092c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt @@ -0,0 +1,266 @@ +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 net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import org.joml.Vector3f + +/** + * 单个 CParticle GPU 粒子的完整生成数据。 + * + * 额外纹理蒙版、生命周期曲线、旋转和 GPU 更新方式都属于每份 data,且本类型只让当前 data + * 进入 CParticle 路径。同一次 + * `genParticles()` 可以混合本类型与普通 [ControlableParticleData]:前者走 GPU,后者继续执行 + * `singleParticleAction` 和 [sign] 对应的 CPU 行为。基础纹理始终来自父类 [effect], + * [textureSource] 只控制叠加在基础纹理上的可选蒙版。 + * + * Example: + * ```kotlin + * val block = ControlableCParticleData().apply { + * textureSource = textureOfBlock(state) + * } + * val item = ControlableCParticleData().apply { + * textureSource = textureOfItem(stack) + * } + * ``` + * Forbidden: 不要把一次 emitter 调用中的公共纹理写回 emitter 单例。 + */ +open class ControlableCParticleData : ControlableParticleData() { + /** + * # STATIC + * 生成后不保留本 data; + * + * # DYNAMIC + * 允许同步可变外观 + * + * 不建议使用DYNAMIC,因为GPU粒子不支持singleAction, + * 这会导致你必须得额外存储每一个粒子的Data,并且按照对应的方式自己修改 + * 非常浪费性能的同时,也不是这个API希望的做法, + * 颜色、生命周期透明度和缩放都已有 STATIC 曲线,不需要为这类变化使用 DYNAMIC。 + * + * 按照GPU粒子的设定一般使用STATIC + * */ + var updateMode: CParticleUpdateMode = CParticleUpdateMode.STATIC + + /** + * 叠加在 [effect] 基础纹理上的可选蒙版;`null` 表示不叠加蒙版。 + * + * Example: 同一 emitter 的不同 CParticle data 可以叠加不同方块或物品纹理。 + * Forbidden: 生成后的 STATIC 粒子不会继续引用或观察此对象。 + */ + var textureSource: CParticleTextureSource? = null + + /** 按 `age / maxAge` 在 GPU 中采样的不透明度乘数曲线。 */ + var alphaCurve: CParticleCurve? = null + + /** + * 按 `age / maxAge` 同时缩放 X/Y 尺寸的等比 GPU 曲线。 + * + * 最终会与 [scaleXCurve]、[scaleYCurve] 的对应轴倍率相乘。 + * Example: `scaleCurve = CParticleCurve.fadeInOut()` 会保持当前宽高比例。 + * Forbidden: 不要用它单独控制某一个轴。 + */ + var scaleCurve: CParticleCurve? = null + + /** + * 按 `age / maxAge` 只缩放 X 方向尺寸的 GPU 曲线。 + * + * 示例:从 `0.2F` 变化到 `1F` 可以让粒子横向展开。 + * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 + */ + var scaleXCurve: CParticleCurve? = null + + /** + * 按 `age / maxAge` 只缩放 Y 方向尺寸的 GPU 曲线。 + * + * 示例:从 `1F` 变化到 `0F` 可以让粒子纵向收拢。 + * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 + */ + var scaleYCurve: CParticleCurve? = null + + /** 按 `age / maxAge` 与基础颜色相乘的 GPU RGB 曲线。 */ + var colorCurve: CParticleColorCurve? = null + + /** ROTATION 模式的指向向量;`null` 时使用父类 yaw/pitch。 */ + var rotationDirection: Vector3f? = null + + /** 欧拉角速度,单位 rad/tick;x=pitch、y=yaw、z=roll。 */ + var angularVelocity: Vector3f = Vector3f() + + /** 每个整数 tick 使用稳定随机种子重新选择纹理动画帧。 */ + var randomAgePreTick: Boolean = false + + /** GPU 随机帧和随机 UV 的种子;`null` 表示生成时自动分配。 */ + var randomSeed: Int? = null + + /** + * 是否使用 CParticle 共享方块占用网格处理本粒子的位移碰撞。 + * + * 碰撞响应与 `moveSingleParticleWithVelocity` 的常用实现一致:命中后停在表面前, + * 并移除速度的法线分量。复杂 `VoxelShape` 会按完整方块近似。 + * 共享缓存覆盖 system 原点附近的 `64³` 方块,分桶边缘至少保留 24 格;缓存外按未命中处理。 + * Example: 同一次 `genParticles()` 可以只给落尘 data 设置为 `true`。 + * Forbidden: 不要用它代替需要台阶、栅栏或实体精确形状的 CPU 碰撞。 + */ + var blockCollision: Boolean = false + + /** 供 Force Command 的 CommandMask 选择。 */ + var commandMask: Int = 0 + + /** metadata 扩展标志位。 */ + var metadataFlags: Int = 0 + + /** 粒子电荷;NaN 表示由 Charge command 使用默认值。 */ + var charge: Float = Float.NaN + + /** 粒子半径,供单点 Lennard-Jones 使用。 */ + var radius: Float = 0F + + /** + * 保存专用 data 的网络编解码器。 + * + * Example: `@CodecField var template = ControlableCParticleData()` 会通过此 codec 同步。 + * Forbidden: 不要用父类 codec 解码本类型,否则 [textureSource] 会丢失。 + */ + companion object { + /** + * 先处理继承的基础字段,再处理可空纹理来源。 + * + * Example: 编解码后 `sign`、velocity、effect 和 textureSource 都保持不变。 + * Forbidden: 字段顺序不能与 [decode] 分离修改。 + */ + @JvmField + val PACKET_CODEC: StreamCodec = + StreamCodec.of(::encode, ::decode) + + /** + * 编码基础字段和当前 data 的可选纹理来源。 + * + * Example: `null` 只写入一个 false,客户端继续使用 effect。 + * Forbidden: 不要在这里解析模型或 UV。 + * + * @param buf 目标网络缓冲区 + * @param data 要编码的 CParticle data + */ + private fun encode(buf: RegistryFriendlyByteBuf, data: ControlableCParticleData) { + encodeBase(buf, data) + val source = data.textureSource + buf.writeBoolean(source != null) + if (source != null) { + CParticleTextureSource.STREAM_CODEC.encode(buf, source) + } + buf.writeByte(data.updateMode.ordinal) + writeNullable(buf, data.alphaCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.scaleCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.scaleXCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.scaleYCurve, CParticleCurve.STREAM_CODEC) + writeNullable(buf, data.colorCurve, CParticleColorCurve.STREAM_CODEC) + val direction = data.rotationDirection + buf.writeBoolean(direction != null) + if (direction != null) buf.writeVector3f(direction) + buf.writeVector3f(data.angularVelocity) + buf.writeBoolean(data.randomAgePreTick) + val seed = data.randomSeed + buf.writeBoolean(seed != null) + if (seed != null) buf.writeInt(seed) + buf.writeBoolean(data.blockCollision) + buf.writeInt(data.commandMask) + buf.writeInt(data.metadataFlags) + buf.writeFloat(data.charge) + buf.writeFloat(data.radius) + } + + /** + * 解码基础字段,并在同一个子类实例上恢复纹理来源。 + * + * Example: Item 来源解码后仍持有独立的 ItemStack 快照。 + * Forbidden: 不能先通过父类 codec 解码后再强制转换。 + * + * @param buf 来源网络缓冲区 + * @return 完整的 CParticle data + */ + private fun decode(buf: RegistryFriendlyByteBuf): ControlableCParticleData { + val data = decodeBase(buf, ControlableCParticleData()) + if (buf.readBoolean()) { + data.textureSource = CParticleTextureSource.STREAM_CODEC.decode(buf) + } + val updateModeOrdinal = buf.readUnsignedByte().toInt() + require(updateModeOrdinal < CParticleUpdateMode.entries.size) { + "unknown CParticle update mode: $updateModeOrdinal" + } + data.updateMode = CParticleUpdateMode.entries[updateModeOrdinal] + data.alphaCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.scaleCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.scaleXCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.scaleYCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) + data.colorCurve = readNullable(buf, CParticleColorCurve.STREAM_CODEC) + if (buf.readBoolean()) data.rotationDirection = buf.readVector3f() + data.angularVelocity = buf.readVector3f() + data.randomAgePreTick = buf.readBoolean() + if (buf.readBoolean()) data.randomSeed = buf.readInt() + data.blockCollision = buf.readBoolean() + data.commandMask = buf.readInt() + data.metadataFlags = buf.readInt() + data.charge = buf.readFloat() + data.radius = buf.readFloat() + return data + } + + private fun writeNullable( + buf: RegistryFriendlyByteBuf, + value: T?, + codec: StreamCodec, + ) { + buf.writeBoolean(value != null) + if (value != null) codec.encode(buf, value) + } + + private fun readNullable( + buf: RegistryFriendlyByteBuf, + codec: StreamCodec, + ): T? = if (buf.readBoolean()) codec.decode(buf) else null + } + + /** + * 返回包含纹理来源的专用 codec。 + * + * Example: emitter 自动编码器可从实例查询此 codec。 + * Forbidden: 返回父类 codec 会截断扩展字段。 + * + * @return [PACKET_CODEC] + */ + override fun getCodec(): StreamCodec = PACKET_CODEC + + /** + * 复制基础字段和当前纹理来源,返回独立的 [ControlableCParticleData]。 + * + * Example: emitter 模板可以为每个生成结果调用 `clone()`。 + * Forbidden: 派生类若有额外字段,必须覆盖本方法,不能依赖本实现保留派生类型。 + * + * @return 带有新 UUID 的 CParticle data + */ + override fun clone(): ControlableCParticleData { + return ControlableCParticleData().also { + copyTo(it) + it.updateMode = updateMode + it.textureSource = textureSource + it.alphaCurve = alphaCurve + it.scaleCurve = scaleCurve + it.scaleXCurve = scaleXCurve + it.scaleYCurve = scaleYCurve + it.colorCurve = colorCurve + it.rotationDirection = rotationDirection?.let(::Vector3f) + it.angularVelocity = Vector3f(angularVelocity) + it.randomAgePreTick = randomAgePreTick + it.randomSeed = randomSeed + it.blockCollision = blockCollision + it.commandMask = commandMask + it.metadataFlags = metadataFlags + it.charge = charge + it.radius = radius + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt new file mode 100644 index 00000000..522c1dd1 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt @@ -0,0 +1,458 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.CooParticlesConstants +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.ParticleTypes +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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: StreamCodec = + StreamCodec.of( + ::encodeBase, + { buf -> decodeBase(buf, ControlableParticleData()) }, + ) + + /** + * 编码所有基础粒子字段,供父类和扩展 data 共用。 + * + * Example: `ControlableCParticleData` 先调用此方法,再写入自己的纹理来源。 + * Forbidden: 子类不能改变这些字段的顺序,否则旧的基础 data 无法解码。 + * + * @param buf 目标网络缓冲区 + * @param data 要编码的基础粒子数据 + */ + internal fun encodeBase(buf: RegistryFriendlyByteBuf, 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) + + ParticleTypes.STREAM_CODEC.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) + } + + /** + * 从缓冲区读取基础字段并写入指定实例。 + * + * Example: 子类传入自己的新实例,解码后仍保留实际运行时类型。 + * Forbidden: 不要在这里固定构造 `ControlableParticleData`,否则子类字段会丢失。 + * + * @param buf 来源网络缓冲区 + * @param target 接收基础字段的实例 + * @return 赋值完成的 [target] + */ + internal fun decodeBase( + buf: RegistryFriendlyByteBuf, + target: T, + ): T { + val uuid = buf.readUUID() + val velocity = buf.readVec3() + val weightSize = buf.readFloat() + val heightSize = buf.readFloat() + val uniformSize = buf.readBoolean() + val visibleRange = buf.readFloat() + val color = buf.readVector3f() + val alpha = buf.readFloat() + val age = buf.readInt() + val maxAge = buf.readInt() + val textureSheet = buf.readUtf() + val effect = ParticleTypes.STREAM_CODEC.decode(buf) as ControlableParticleEffect + effect.controlUUID = uuid + val speed = buf.readDouble() + val speedLimit = buf.readDouble() + val sign = buf.readInt() + val light = buf.readInt() + val cameraOption = ParticleCameraOption.STREAM_CODEC.decode(buf) + val axis = buf.readVec3() + val yaw = buf.readFloat() + val pitch = buf.readFloat() + val roll = buf.readFloat() + val depthSize = buf.readFloat() + return target.apply { + this.uuid = uuid + this.velocity = velocity + this.color = color + this.alpha = alpha + this.uniformSize = uniformSize + this.weightSize = weightSize + this.heightSize = heightSize + this.depthSize = depthSize + this.visibleRange = visibleRange + this.age = age + this.maxAge = maxAge + this.textureSheet = textureSheet + this.effect = effect + this.speed = speed + this.sign = sign + this.speedLimit = speedLimit + this.light = light + this.cameraOption = cameraOption + this.axis = axis + this.yaw = yaw + this.pitch = pitch + this.roll = roll + } + } + + @JvmStatic + fun registerRenderType(type: ParticleRenderType) { + particleTexturesMapper[type.toString()] = type + } + } + + + /** + * 粒子生成时会传输的控制UUID + */ + var uuid = UUID.randomUUID() + + /** + * 粒子的移动向量 + * 在粒子发射器中 会不断调用这次的参数 + * + * 此选项会一直赋值给实际粒子 + */ + var velocity: Vec3 = Vec3.ZERO + + /** + * 生成的粒子相机朝向模式,默认始终面向摄像头。 + */ + var cameraOption: ParticleCameraOption = ParticleCameraOption.BILLBOARD + + /** 旧布尔 API 的兼容桥接:true = BILLBOARD,false = ROTATION。 */ + var faceToCamera: Boolean + get() = cameraOption == ParticleCameraOption.BILLBOARD + set(value) { + cameraOption = ParticleCameraOption.fromFaceToCamera(value) + } + + /** AXIS_BILLBOARD 使用的固定轴方向。 */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 如果faceToCamera为false + * 则此参数代表了粒子水平朝向 + * + * 弧度制 + */ + var yaw = 0.0f + + /** + * 如果faceToCamera为false + * 则此参数代表了粒子垂直朝向 + * + * 弧度制 + * + */ + var pitch = 0.0f + + /** + * 此参数代表了粒子滚动朝向 + * + * 弧度制 + * + */ + var roll = 0.0f + + /** 是否保持宽高等比。 */ + var uniformSize = true + + /** 粒子宽度大小。 */ + private var currentWeightSize = 0.2f + var weightSize: Float + get() = currentWeightSize + set(value) { + currentWeightSize = value + if (uniformSize) { + currentHeightSize = value + } + } + + /** 粒子高度大小。 */ + private var currentHeightSize = 0.2f + var heightSize: Float + get() = currentHeightSize + set(value) { + currentHeightSize = value + if (uniformSize) { + currentWeightSize = value + } + } + + private var currentDepthSize = 0f + var depthSize: Float + get() = currentDepthSize + set(value) { + currentDepthSize = value + } + + /** 旧 size 兼容属性:设置时同时修改宽高。 */ + var size: Float + get() = (weightSize + heightSize) / 2f + set(value) { + currentWeightSize = value + currentHeightSize = value + } + + /** + * 粒子生成时采用的不透明度 + * + */ + var alpha = 1f + + /** + * 粒子生成时设置的age + * + */ + var age = 0 + + /** + * 粒子最大生命周期 + * + */ + var maxAge = 120 + + /** + * 粒子生成时的亮度 + * (修改粒子亮度时请修改该数据) + * + */ + var light = 15 + + /** + * 粒子可见范围 + */ + var visibleRange = 256f + + /** + * 粒子样式 (必须是可控制的粒子) + * + * 此选项只生效一次 + */ + var effect: ControlableParticleEffect = ControlableEndRodEffect(uuid) + + /** + * 一些特殊标识 + * 用于在single 区分不同类型 分工的粒子 + */ + var sign = 0 + + /** + * 粒子移动速度上限 + * 防止不知道什么原因导致粒子移速过高从而导致客户端卡死 + */ + var speedLimit = 32.0 + + /** + * 粒子生成时采用的颜色,后续控制不生效 + * + * 此选项只应用一次 + */ + var color = Vector3f(1f, 1f, 1f) + + /** + * 粒子重生次数 + * 他会在不断的粒子重生中递增 + */ + var respawnCount = 0 + internal set + + // 脑瘫东西设置了客户端专属 + // 粒子渲染方式 只生效一次 + private var textureSheet: String = "PARTICLE_SHEET_TRANSLUCENT" + + /** + * 粒子移动速度 + * 在ClassParticlesEmitters默认不生效 + */ + var speed: Double = 1.0 + fun textureSheetFromString(sheet: String): ParticleRenderType? { + return particleTexturesMapper[sheet] + } + + /** + * 快速旋转 yaw pitch 到目标点 + * + * @param to 目标相对位置 + */ + fun setRotationTo(to: Vector3f) { + val (x, y, z) = Math3DUtil.calculateEulerAnglesToPoint(to) + this.yaw = y + this.pitch = x + } + + /** + * 快速旋转 yaw pitch 到目标点 + * + * @param to 目标相对位置 + */ + fun setRotationTo(to: Vec3) { + setRotationTo(to.toVector3f()) + } + + /** + * 快速旋转 yaw pitch 到目标点 + * + * @param to 目标相对位置 + */ + fun setRotationTo(to: RelativeLocation) { + setRotationTo(to.toVector3f()) + } + + fun setAxisLocation(axis: RelativeLocation) { + this.axis = Vec3(axis.x, axis.y, axis.z) + } + + fun getTextureSheet(): ParticleRenderType { + return textureSheetFromString(textureSheet) ?: let { + CooParticlesConstants.logger.error("can not find textureSheet $textureSheet you need use ControlableParticleData.registerRenderType() to register mapper") + ParticleRenderType.PARTICLE_SHEET_OPAQUE + } + } + + /** + * 如果你的参数暴露在外面 可能会被服务器调用 + * 则使用这个 + * + * 输入的参数是你的目标的RenderType的toString的内容 + * + * @param value + */ + fun setTextureSheet(value: String) { + this.textureSheet = value + } + + fun setTextureSheet(value: TextureSheetsEnum){ + this.textureSheet = value.name + } + + /** + * 如果你的参数暴露在外面 (可能会被服务器环境调用) + * 那就不要使用这个方法 使用字符串的 + * 因为脑残的设计导致服务器无法访问 ParticleRenderType 类 + * [setTextureSheet(String)] + * + * @param value + */ + fun setTextureSheet(value: ParticleRenderType) { + this.textureSheet = value.toString() + } + + override fun getCodec(): StreamCodec { + return PACKET_CODEC + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + val control = ControlParticleManager.createControl(effect.controlUUID) + val data = this + control.applyInitializedAction { + this.uniformSize = data.uniformSize + this.weightSize = data.weightSize + this.heightSize = data.heightSize + this.depthSize = data.depthSize + this.color = data.color + this.currentAge = data.age + this.lifetime = data.maxAge + this.light = data.light + this.textureSheet = data.getTextureSheet() + this.particleAlpha = data.alpha + this.cameraOption = data.cameraOption + this.axis = data.axis + this.previewAxis = data.axis + this.previewWeightSize = data.weightSize + this.previewHeightSize = data.heightSize + this.previewDepthSize = data.depthSize + this.currentPitch = data.pitch + this.currentYaw = data.yaw + this.currentRoll = data.roll + this.previewPitch = data.pitch + this.previewYaw = data.yaw + this.previewRoll = data.roll + } + return control + } + + override fun getDisplayer(): ParticleDisplayer { + return ParticleDisplayer.withSingle(effect) + } + + override fun clone(): ControlableParticleData { + return ControlableParticleData().also(::copyTo) + } + + /** + * 把可克隆的基础字段复制到另一个 data,并为目标生成新 UUID。 + * + * Example: 扩展类可调用 `super.copyTo(target)` 后补充自己的字段。 + * Forbidden: 网络解码不能调用此方法,因为网络中的 UUID 必须原样保留。 + * + * @param target 接收当前基础字段的实例 + */ + protected fun copyTo(target: ControlableParticleData) { + target.uuid = UUID.randomUUID() + target.velocity = velocity + target.uniformSize = uniformSize + target.weightSize = weightSize + target.heightSize = heightSize + target.depthSize = depthSize + target.color = color + target.alpha = alpha + target.visibleRange = visibleRange + target.age = age + target.maxAge = maxAge + target.effect = effect.clone() + target.textureSheet = textureSheet + target.speed = speed + target.sign = sign + target.speedLimit = speedLimit + target.light = light + target.yaw = yaw + target.pitch = pitch + target.roll = roll + target.cameraOption = cameraOption + target.axis = axis + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt new file mode 100644 index 00000000..762af1a4 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt @@ -0,0 +1,144 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +import cn.coostack.cooparticlesapi.api.controler.Controlable +import cn.coostack.cooparticlesapi.api.controler.SerializableData +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.display.DisplayEntityManager +import cn.coostack.cooparticlesapi.display.DisplayEntity +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import io.netty.buffer.Unpooled +import net.minecraft.core.RegistryAccess +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +/** + * SerializableData wrapper for DisplayEntity emitters entry. + */ +class DisplayEntityEmittersData( + var entityType: String = "", + var entityData: ByteArray = ByteArray(0), + var visibleRange: Float = 256f +) : SerializableData { + constructor(entity: DisplayEntity) : this() { + setEntity(entity) + } + + companion object { + @JvmStatic + val PACKET_CODEC: StreamCodec = + StreamCodec.of( + { buf, data -> + buf.writeUtf(data.entityType) + buf.writeInt(data.entityData.size) + buf.writeBytes(data.entityData) + buf.writeFloat(data.visibleRange) + }, + { buf -> + val type = buf.readUtf() + val size = buf.readInt() + val bytes = ByteArray(size) + buf.readBytes(bytes) + val visibleRange = buf.readFloat() + DisplayEntityEmittersData(type, bytes, visibleRange).also { + it.registryAccess = buf.registryAccess() + } + } + ) + + @JvmStatic + fun fromEntity(entity: DisplayEntity): DisplayEntityEmittersData { + val registryAccess = entity.world?.registryAccess() ?: registryAccessFallback() + val buf = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) + entity.getCodec().encode(buf, entity) + val data = ByteArray(buf.readableBytes()) + buf.readBytes(data) + return DisplayEntityEmittersData(entity::class.java.name, data).also { + it.registryAccess = registryAccess + } + } + + private fun registryAccessFallback(): RegistryAccess { + return CooParticlesAPI.registryAccessOrNull + ?: RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY) + } + } + + private var template: DisplayEntity? = null + private var preparedControler: DisplayEntity? = null + private var registryAccess: RegistryAccess? = null + + private fun decodeEntity(registryAccess: RegistryAccess): DisplayEntity { + val codec = DisplayEntityManager.registeredTypes[entityType] + ?: throw IllegalStateException("DisplayEntity codec not registered for type: $entityType") + return codec.decode( + RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(entityData.copyOf()), registryAccess) + ) + } + + private fun cloneEntity(entity: DisplayEntity): DisplayEntity { + val type = entity::class.java + val ins = runCatching { + type.getDeclaredConstructor(Vec3::class.java, Level::class.java) + .apply { isAccessible = true } + .newInstance(entity.pos, entity.world) + }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() + + return ins.apply { update(entity) } + } + + private fun resolveEntity(registryAccess: RegistryAccess? = null): DisplayEntity { + return template ?: decodeEntity( + registryAccess ?: this.registryAccess ?: registryAccessFallback() + ).also { template = it } + } + + private fun createEntity(registryAccess: RegistryAccess? = null): DisplayEntity { + return cloneEntity(resolveEntity(registryAccess)) + } + + override fun getCodec(): StreamCodec { + return PACKET_CODEC + } + + override fun clone(): SerializableData { + val entity = runCatching { cloneEntity(resolveEntity()) }.getOrNull() + return if (entity != null) { + DisplayEntityEmittersData(entity).also { it.visibleRange = visibleRange } + } else { + DisplayEntityEmittersData(entityType, entityData.copyOf(), visibleRange) + } + } + + override fun createControler( + world: ClientLevel, + pos: Vec3, + particleLerpProcess: Float, + posLerpProcess: Float + ): Controlable<*> { + val entity = createEntity(world.registryAccess()) + entity.world = world + entity.pos = pos + preparedControler = entity + return entity + } + + override fun getDisplayer(): ParticleDisplayer { + val entity = preparedControler ?: createEntity().also { preparedControler = it } + preparedControler = null + return ParticleDisplayer.withDisplayEntity(entity) + } + + fun setEntity(entity: DisplayEntity): DisplayEntityEmittersData { + val clone = cloneEntity(entity) + val data = fromEntity(clone) + entityType = data.entityType + entityData = data.entityData + template = clone + preparedControler = null + return this + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt new file mode 100644 index 00000000..9f8cf37b --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt @@ -0,0 +1,5 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +fun interface ParticleDataFactory { + fun create(): ControlableParticleData +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt new file mode 100644 index 00000000..4e28caa7 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt @@ -0,0 +1,122 @@ +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.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.server.level.ServerLevel +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + + +/** + * 发射单个粒子的 粒子发射器 + * + * 单个粒子通过 ParticleEmitters 修改粒子参数 + * + * -> 不固定数量的ParticleStyleData + * -> 方便输入的 + */ +interface ParticleEmitters : ServerControler, NetworkDirtyMarkable { + var pos: Vec3 + var world: Level? + var tick: Int + + /** + * 当maxTick == -1时 + * 代表此粒子不会由生命周期控制 + */ + var maxTick: Int + var delay: Int + var uuid: UUID + var canceled: Boolean + var playing: Boolean + + /** + * @param innerClass 是否在类内添加 + * 在类内添加的event handler不会参与codec传输 + */ + 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) + + /** + * 注册时传入的CODEC必须和这个完全一致 + * + * 编解码器 + * + * 编码粒子信息, 当前位置 + */ + fun getCodec(): StreamCodec + + override fun getValue(): ParticleEmitters { + return this + } + + /** + * 标记发射器的完整网络状态需要更新。 + * + * Manager 会在当前服务端 tick 结束时合并发送,同一发射器一 tick 内只会更新一次。 + */ + 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt new file mode 100644 index 00000000..e374097e --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt @@ -0,0 +1,398 @@ +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.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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 { + // 已经start的 emitters + val emittersCodec = HashMap>() + + /** + * 服务器拥有 + */ + val serverEmitters = HashMap() + internal val visible = ConcurrentHashMap>() + private val dirtyEmitters = ConcurrentHashMap.newKeySet() + + /** + * 客户端可视 + */ + val clientEmitters = ConcurrentHashMap() + + /** 返回客户端当前可见 Emitter 数。 */ + fun clientEmitterCount(): Int = clientEmitters.size + + /** 返回服务端当前 Emitter 数。 */ + fun serverEmitterCount(): Int = serverEmitters.size + + fun getCodecFromID(id: String): StreamCodec? { + return emittersCodec[id] + } + + + /** + * 在客户端执行 + */ + @JvmStatic + fun register( + id: String, + codec: StreamCodec + ): StreamCodec { + 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 + } + + /** + * 玩家离开服务器时清理其 Emitter 可见缓存,使重连后重新发送创建包。 + * + * @param player 已断开连接的服务端玩家 + */ + 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 registryAccess = emitters.world?.registryAccess() ?: CooParticlesAPI.registryAccessOrNull ?: return ByteArray(0) + val buf = RegistryFriendlyByteBuf( + Unpooled.buffer(), + registryAccess + ) + 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 + } + // 获取instance + 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt new file mode 100644 index 00000000..7c9e5981 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt @@ -0,0 +1,23 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters + +object PhysicConstant { + /** + * 时间单位 tick + */ + const val EARTH_GRAVITY = 0.05 + + /** + * 海平面空气密度 + */ + const val SEA_AIR_DENSITY = 1.225 + + /** + * 粒子物理拖动系数 + */ + const val DRAG_COEFFICIENT = 0.01 + + /** + * 粒子横截面积 + */ + const val CROSS_SECTIONAL_AREA = 0.01 +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt new file mode 100644 index 00000000..8fda5182 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt @@ -0,0 +1,270 @@ +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.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import org.joml.Vector3f +import kotlin.math.roundToInt +import kotlin.random.Random + +/** + * 防止老是要手写等 一堆莫名其妙的随机数据而设立的 + * @param maxAge 粒子最大生命周期 + * @param minAge 粒子最小生命周期 + * @param maxCount 粒子最大个数 + * @param minCount 粒子最小个数 + * @param maxSize 粒子最大尺寸 + * @param minSize 粒子最小尺寸 + * @param maxSpeed 粒子最大速度 + * @param minSpeed 粒子最小速度 + * + */ +class SimpleRandomParticleData { + + companion object { + val PACKET_CODEC = StreamCodec.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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt new file mode 100644 index 00000000..39fb67ac --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt @@ -0,0 +1,493 @@ +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.FriendlyByteBuf +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 + +/** + * 只生成 [ControlableCParticleData] 的可变换 GPU 粒子发射器。 + * + * 每个实例拥有独立的 GPU 坐标空间。[space] 为 [CParticleEmitterSpace.LOCAL] 时,已经生成的粒子会随 + * 发射器一起平移、按 [emitterRotation] 旋转并等比缩放;[particleRotation] 只控制新粒子的出生方向。 + * 切换到 [CParticleEmitterSpace.WORLD] 后,新粒子改用世界坐标, + * 行为与 [ClassParticleEmitters] 的 GPU 路径一致。两类粒子使用不同 system,切换模式不会移动旧粒子。 + * + * 本类型直接实现 [ParticleEmitters],不继承 [ClassParticleEmitters]。逐粒子控制器、粒子事件和死亡重生 + * 不属于 GPU system 能力,因此不提供 CPU 回退。 + * + * @param pos 发射器世界坐标 + * @param world 发射器所在世界 + */ +abstract class TransformableCParticleEmitter( + pos: Vec3, + override var world: Level?, +) : ParticleEmitters { + + /** 发射器当前的世界坐标;直接修改后最迟在下一次 tick 同步。 */ + private val posState = dirty(pos) + override var pos by posState + + /** 当前生命周期 tick。 */ + override var tick: Int = 0 + + /** 生命周期上限;`-1` 表示不按 tick 自动结束。 */ + override var maxTick: Int = 120 + + /** 两次发射之间的 tick 间隔;小于 `1` 时按每 tick 发射处理。 */ + override var delay: Int = 0 + + /** 跨服务端和客户端识别同一发射器的 UUID。 */ + override var uuid: UUID = UUID.randomUUID() + + /** 是否已经结束生命周期。 */ + override var canceled: Boolean = false + + /** 是否已经启动。 */ + override var playing: Boolean = false + + /** 新粒子使用的坐标空间。切换不会改变已经生成的粒子。 */ + var space: CParticleEmitterSpace = CParticleEmitterSpace.LOCAL + + /** LOCAL 粒子所在的发射器空间旋转;变化时会带动已经生成的粒子。 */ + var emitterRotation: Quaternionf = Quaternionf() + set(value) { + require(value.isFinite && value.lengthSquared() > 1e-12F) { + "rotation must be finite and non-zero" + } + field = Quaternionf(value).normalize() + } + + /** 新生成粒子的出生位置和初速度旋转;不会改变已经生成的粒子。 */ + var particleRotation: Quaternionf = Quaternionf() + set(value) { + require(value.isFinite && value.lengthSquared() > 1e-12F) { + "particle rotation must be finite and non-zero" + } + field = Quaternionf(value).normalize() + } + + /** 发射器局部空间的等比缩放。必须为正数,保证 GPU 模拟可以换算回局部空间。 */ + var scale: Double = 1.0 + set(value) { + require(value.isFinite() && value > 0.0) { "scale must be finite and positive" } + field = value + } + + /** 环境空气密度,用于生成 GPU 阻力。 */ + var airDensity: Double = 0.0 + + /** 每 tick 向世界 Y 负方向施加的重力加速度。 */ + var gravity: Double = 0.0 + + /** 与 [ClassParticleEmitters] 保持一致的质量数据;当前 GPU 力场不会读取它。 */ + var mass: Double = 1.0 + + /** 风力方向。GPU 路径只支持非 relative 的全局风。 */ + var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { it.loadEmitters(this) } + + /** 是否沿发射器上一 tick 到当前 tick 的路径插值生成。 */ + var enableInterpolator: Boolean = false + + /** 发射路径插值器。 */ + var emittersInterpolator: Interpolator = LineEmitterInterpolator().setRefiner(5.0) + + /** 发射器在当前 tick 中产生的世界坐标位移。 */ + var emitterVelocity: Vec3 = Vec3.ZERO + private set + + private var lastTickPos: Vec3 = pos + private var lastSyncedTransform = TransformSnapshot.capture(this) + + /** + * 旧版 GPU 力场列表接口。 + * + * 该类不继承 [ClassParticleEmitters],因此单独保留旧签名。 + * 默认实现由流式接口转发,旧 emitter 覆写此方法仍能参与 GPU 模拟。 + * + * @return 按执行顺序排列的力场列表 + */ + open fun cparticleForces(): List = emptyList() + + /** + * 返回当前 GPU system 使用的附加力场。 + * + * 位置和方向参数都沿用 [ClassParticleEmitters] 的世界坐标语义。 + * 默认实现兼容旧版 [cparticleForces];新 emitter 可直接提交到 sink。 + * + * @param sink 当前 emitter 的力场快照 + */ + open fun submitCParticleForces(sink: CParticleForceSink) { + sink.submitAll(cparticleForces()) + } + + /** 返回方块碰撞网格相对当前发射器中心的保证范围。 */ + open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE + + /** + * 拒绝注册逐粒子事件。 + * + * @param handler 待注册事件处理器 + * @param innerClass 是否为实现类内部处理器 + * @throws UnsupportedOperationException 本发射器没有逐粒子 CPU 控制器 + */ + final override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { + throw UnsupportedOperationException("TransformableCParticleEmitter does not support per-particle events") + } + + override fun start() { + if (playing) return + playing = true + lastTickPos = pos + emitterVelocity = Vec3.ZERO + lastSyncedTransform = TransformSnapshot.capture(this) + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + override fun stop() { + canceled = true + } + + override fun tick() { + if (canceled || !playing) return + val level = world ?: return + doTick() + emitterVelocity = pos - lastTickPos + lastTickPos = pos + + if (!level.isClientSide) { + syncServerTransformIfChanged() + increaseTick() + return + } + + TransformableCParticleEmitterBridge.syncSystems(this) + if (enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + if (tick % max(1, delay) == 0) { + if (enableInterpolator) { + val refined = emittersInterpolator.getRefinedResult() + val denominator = (refined.size - 1).coerceAtLeast(1).toFloat() + refined.forEachIndexed { index, location -> + val spawnPos = location.toVector() + val lerpProgress = if (refined.size == 1) 1F else index / denominator + doSubtick(spawnPos, lerpProgress) + spawnParticle(spawnPos, lerpProgress) + } + } else { + spawnParticle(pos, 1F) + } + } + increaseTick() + } + + override fun spawnParticle(pos: Vec3, lerpProgress: Float) { + val level = world as? ClientLevel ?: return + val player = Minecraft.getInstance().player ?: return + val particles = genParticles(lerpProgress) + val batchSize = particles.size + particles.forEach { (data, relative) -> + val worldPosition = TransformableCParticleEmitterBridge.resolveWorldPosition( + this, + pos, + relative, + ) ?: return@forEach + if (player.position().distanceTo(worldPosition) > data.visibleRange) return@forEach + TransformableCParticleEmitterBridge.trySpawn( + this, + level, + pos, + relative, + data, + batchSize, + ) + } + } + + /** + * 服务器和客户端每 tick 都会调用,用于更新发射器自身状态。 + * + * 实现必须保证两端执行结果可同步,不能在这里直接访问仅客户端存在的渲染对象。 + */ + abstract fun doTick() + + /** + * 生成本次发射的 GPU 粒子及其相对位置。 + * + * LOCAL 模式下相对位置属于发射器坐标空间;WORLD 模式下会直接加到生成世界坐标。 + * + * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` + * @return 本次要生成的 GPU data 和位置 + */ + abstract fun genParticles(lerpProgress: Float): List> + + /** + * 在路径插值中的一次发射前调用。 + * + * @param current 本次插值后的发射世界坐标 + * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` + */ + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} + + /** + * 追加发射器空间旋转。 + * + * LOCAL 模式下,已经生成的粒子位置和速度都会跟随整个空间旋转。 + * + * @param rotation 发射器空间的增量四元数 + */ + fun rotateEmitter(rotation: Quaternionfc) { + emitterRotation = Quaternionf(emitterRotation).mul(rotation) + syncClientTransform() + } + + /** 绕发射器局部 Z 轴追加旋转。 */ + fun rotateEmitter(radian: Double) { + rotateEmitter(Quaternionf().rotateLocalZ(radian.toFloat())) + } + + /** + * 追加新粒子的出生旋转。 + * + * 只改变后续新粒子的出生位置和初速度;已经生成的粒子保持生成时的运动方向。 + * 粒子四边形朝向仍只由 `cameraOption` 和粒子自身旋转字段决定。 + * + * @param rotation 新粒子出生坐标系的增量四元数 + */ + fun rotateParticle(rotation: Quaternionfc) { + particleRotation = Quaternionf(particleRotation).mul(rotation) + syncClientTransform() + } + + /** 绕新粒子出生坐标系的局部 Z 轴追加旋转。 */ + fun rotateParticle(radian: Double) { + rotateParticle(Quaternionf().rotateLocalZ(radian.toFloat())) + } + + /** + * 按世界坐标增量移动发射器。 + * + * @param offset 本次增加的世界坐标位移 + */ + fun translate(offset: Vec3) { + teleportTo(pos + offset) + } + + /** + * 按世界坐标分量增量移动发射器。 + * + * @param x X 轴增量 + * @param y Y 轴增量 + * @param z Z 轴增量 + */ + fun translate(x: Double, y: Double, z: Double) { + translate(Vec3(x, y, z)) + } + + /** + * 设置绝对等比缩放。 + * + * @param new 大于零的缩放值 + */ + fun scale(new: Double) { + scale = new + syncClientTransform() + } + + /** + * 按倍率增量缩放。 + * + * @param factor 大于零的缩放倍率 + */ + fun scaleBy(factor: Double) { + require(factor.isFinite() && factor > 0.0) { "scale factor must be finite and positive" } + scale(scale * factor) + } + + override fun teleportTo(to: Vec3) { + pos = to + syncClientTransform() + } + + override fun rotateToPoint(to: RelativeLocation) { + val next = Quaternionf() + Math3DUtil.rotateQuatToPoint(next, to) + emitterRotation = next + syncClientTransform() + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { + val next = Quaternionf() + Math3DUtil.rotateQuatToPoint(next, to) + next.rotateLocalZ(radian.toFloat()) + emitterRotation = next + syncClientTransform() + } + + override fun rotateAsAxis(radian: Double) { + rotateEmitter(radian) + } + + override fun update(emitters: ParticleEmitters) { + if (emitters !is TransformableCParticleEmitter || getEmittersID() != emitters.getEmittersID()) return + posState.setCodecValue(emitters.pos) + world = world ?: emitters.world + tick = emitters.tick + maxTick = emitters.maxTick + delay = emitters.delay + uuid = emitters.uuid + canceled = emitters.canceled + playing = emitters.playing + space = emitters.space + emitterRotation = Quaternionf(emitters.emitterRotation) + particleRotation = Quaternionf(emitters.particleRotation) + scale = emitters.scale + gravity = emitters.gravity + airDensity = emitters.airDensity + mass = emitters.mass + wind = emitters.wind.also { it.loadEmitters(this) } + enableInterpolator = emitters.enableInterpolator + emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) + ParticleEmittersRegistryHelper.updateEmitter(this, emitters) + resetRuntimeState(addInterpolatorPoint = false) + syncClientTransform() + } + + /** 结束后停止接收新粒子,并在已有粒子死亡后释放专属 GPU systems。 */ + internal fun finishClientSystems() { + TransformableCParticleEmitterBridge.finishEmitter(uuid) + } + + private fun syncClientTransform() { + if (world?.isClientSide == true) { + TransformableCParticleEmitterBridge.syncSystems(this) + } + } + + private fun syncServerTransformIfChanged() { + val current = TransformSnapshot.capture(this) + if (current == lastSyncedTransform) return + markDirty() + lastSyncedTransform = current + } + + private fun increaseTick() { + if (++tick >= maxTick && maxTick != -1) { + stop() + } + } + + private fun resetRuntimeState(addInterpolatorPoint: Boolean) { + lastTickPos = pos + emitterVelocity = Vec3.ZERO + lastSyncedTransform = TransformSnapshot.capture(this) + if (addInterpolatorPoint && enableInterpolator) { + emittersInterpolator.insertPoint(pos) + } + } + + private data class TransformSnapshot( + val pos: Vec3, + val rotationX: Float, + val rotationY: Float, + val rotationZ: Float, + val rotationW: Float, + val particleRotationX: Float, + val particleRotationY: Float, + val particleRotationZ: Float, + val particleRotationW: Float, + val scale: Double, + val space: CParticleEmitterSpace, + ) { + companion object { + fun capture(emitter: TransformableCParticleEmitter): TransformSnapshot { + val rotation = emitter.emitterRotation + return TransformSnapshot( + emitter.pos, + rotation.x, + rotation.y, + rotation.z, + rotation.w, + emitter.particleRotation.x, + emitter.particleRotation.y, + emitter.particleRotation.z, + emitter.particleRotation.w, + emitter.scale, + emitter.space, + ) + } + } + } + + companion object { + /** 编码所有可变换 GPU emitter 共用的网络字段。 */ + fun encodeBase(data: TransformableCParticleEmitter, buf: FriendlyByteBuf) { + 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.writeByte(data.space.networkId) + buf.writeQuaternion(data.emitterRotation) + buf.writeQuaternion(data.particleRotation) + buf.writeDouble(data.scale) + 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) + } + + /** 解码所有可变换 GPU emitter 共用的网络字段。 */ + fun decodeBase(container: TransformableCParticleEmitter, buf: FriendlyByteBuf) { + container.posState.setCodecValue(buf.readVec3()) + container.tick = buf.readInt() + container.maxTick = buf.readInt() + container.delay = buf.readInt() + container.uuid = buf.readUUID() + container.canceled = buf.readBoolean() + container.playing = buf.readBoolean() + container.space = CParticleEmitterSpace.fromNetworkId(buf.readUnsignedByte().toInt()) + container.emitterRotation = buf.readQuaternion() + container.particleRotation = buf.readQuaternion() + container.scale = buf.readDouble() + container.gravity = buf.readDouble() + container.airDensity = buf.readDouble() + container.mass = buf.readDouble() + container.enableInterpolator = buf.readBoolean() + container.emittersInterpolator.setRefiner(buf.readDouble()) + val windId = buf.readUtf() + container.wind = WindDirections.getCodecFromID(windId).decode(buf).also { + it.loadEmitters(container) + } + container.resetRuntimeState(addInterpolatorPoint = true) + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt new file mode 100644 index 00000000..91991286 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt @@ -0,0 +1,30 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class OrbitMode { + /** + * 物理轨道(默认) + * + * - 切向 + 径向纠正 + * - 手感最真实 + * - 需要限幅,否则启动阶段可能不稳定 + */ + PHYSICAL, + + /** + * 弹簧轨道 + * + * - 半径误差像弹簧一样回弹 + * - 非常稳定,不容易飞走 + * - 手感偏“设计感” + */ + SPRING, + + /** + * 吸附轨道 + * + * - 每 tick 直接把粒子拉到目标半径附近 + * - 最稳定,最不物理 + * - 适合魔法阵 / UI / 装饰粒子 + */ + SNAP +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt new file mode 100644 index 00000000..eeee7078 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt @@ -0,0 +1,128 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.max + +/** + * 吸引/排斥力(指向目标点的加速度) + * + * - strength > 0:吸引(往目标点拉) + * - strength < 0:排斥(从目标点推开) + * - falloff 决定“近强远弱”的衰减方式 + */ +class ParticleAttractionCommand() : ParticleCommand { + + /** 目标点(世界坐标)。 */ + var target: Supplier = Supplier { Vec3.ZERO } + + /** + * 强度(每 tick 的速度增量基准)。 + * + * - 0.1 ~ 0.5:轻微牵引(像磁力) + * - 0.5 ~ 3.0:明显吸附(像黑洞) + * - 3.0+:非常暴力(需要配合 speedLimit / damping) + * + * 负数则变成排斥力。 + */ + var strength: Double = 0.8 + + /** + * 有效范围尺度(>0)。 + * + * - 越大:衰减越慢,远处也能感受到吸力 + * - 越小:只有靠近目标才明显 + */ + var range: Double = 8.0 + + /** + * 衰减幂次(>=1)。 + * + * - 1:衰减较缓 + * - 2:更像“平方反比”趋势 + * - 3:近距离极强,远处几乎无 + */ + var falloffPower: Double = 2.0 + + /** + * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 + * + * - 建议 0.1 ~ 1.0 + */ + var minDistance: Double = 0.25 + + /** + * @param target 目标点(世界坐标) + * @param strength 强度(>0吸引 <0排斥) + * @param range 范围尺度,越大影响越远 + * @param falloffPower 衰减幂次(>=1) + * @param minDistance 最小距离钳制,防止爆炸/NaN + */ + constructor( + target: Supplier = Supplier { Vec3.ZERO }, + strength: Double = 0.8, + range: Double = 8.0, + falloffPower: Double = 2.0, + minDistance: Double = 0.25, + ) : this() { + this.target = target + this.strength = strength + this.range = range + this.falloffPower = falloffPower + this.minDistance = minDistance + } + + /** 目标点(世界坐标)。 */ + fun target(v: Supplier) = apply { target = v } + + /** + * 强度(每 tick 的速度增量基准)。 + * + * - 0.1 ~ 0.5:轻微牵引(像磁力) + * - 0.5 ~ 3.0:明显吸附(像黑洞) + * - 3.0+:非常暴力(需要配合 speedLimit / damping) + * + * 负数则变成排斥力。 + */ + fun strength(v: Double) = apply { strength = v } + + /** + * 有效范围尺度(>0)。 + * + * - 越大:衰减越慢,远处也能感受到吸力 + * - 越小:只有靠近目标才明显 + */ + fun range(v: Double) = apply { range = v } + + /** + * 衰减幂次。 + * + * - 1:衰减较缓 + * - 2:更像“平方反比”趋势 + * - 3:近距离极强,远处几乎无 + */ + fun falloffPower(v: Double) = apply { falloffPower = v } + + /** + * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 + * + * - 建议 0.1 ~ 1.0 + */ + fun minDistance(v: Double) = apply { minDistance = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val dir = target.get().subtract(pos) + var d = dir.length() + if (d < 1e-9) return + d = max(d, minDistance) + + val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) + val dv = dir.normalize().scale(strength * falloff) + + data.velocity = data.velocity.add(dv) + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt new file mode 100644 index 00000000..74acb430 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt @@ -0,0 +1,46 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle + + +fun interface ParticleCommand { + /** + * 粒子“命令/模块”接口:用于在粒子每个 tick 更新前,对粒子数据进行一次可组合的逻辑处理。 + * + * ## 设计目的 + * 你可以把它理解成 **Unity 粒子系统的一个 Module**(例如:旋转力、阻尼、吸引力、漩涡、流场等): + * - 每个 module 都实现一个 `execute(...)` + * - 在粒子 tick 之前按顺序调用这些 module + * - module 之间通过“修改同一个 data”来叠加效果(通常是 `data.velocity`、`data.yaw/pitch/roll` 等) + * + * ## 调用时机(强烈建议) + * 推荐在 `Emitter.singleParticleAction(...)` 中,将命令挂载到 `ParticleControler.addPreTickAction { ... }`: + * - 这样命令会在 **每个 tick** 都执行一次(而不是只在生成时执行一次) + * - 且执行时你能拿到 “实时的粒子实例状态”(`particle.loc`, `particle.onTheGround`, `particle.currentAge` 等) + * + * ## 参数说明 + * @param data + * 可变的粒子控制数据(你自己的控制层数据)。 + * - **常见修改点**:`data.velocity`(加速度/阻尼/流场)、`data.faceToCamera`、`data.yaw/pitch/roll` 等 + * - 注意:`ControlableParticleData` 中有一部分字段在粒子生成后只生效一次(例如颜色/纹理等), + * 因此命令更适合修改“每 tick 生效”的字段(通常是 velocity 与旋转等)。 + * + * @param particle + * 当前 tick 正在更新的粒子实例(客户端真实粒子对象)。 + * - **常用读取**:`particle.loc`(位置)、`particle.onTheGround`(是否落地)、`particle.currentAge`(年龄)等 + * - **注意**:一般不建议在命令里直接 teleport 粒子位置;更推荐通过 `data.velocity` 影响运动, + * 由发射器的 physics/move 逻辑统一处理。 + * + * ## 组合与顺序 + * 多个命令按添加顺序执行: + * - 前面的命令可能会影响后面的命令输入(例如先吸引再阻尼 vs 先阻尼再吸引) + * - 如果你在命令里做了 clamp/limit(例如限速),通常建议放到最后一个命令执行 + * + * ## 编写建议(实践约定) + * - **保持轻量**:命令每 tick 对每个粒子都会执行,避免昂贵计算/分配对象(尤其是大量粒子场景) + * - **不要产生 NaN**:对距离接近 0 的情况要做钳制(例如 minDistance) + * - **可复用**:命令实例最好可以被多个粒子共享(内部不要缓存“单粒子状态”) + */ + fun execute(data: ControlableParticleData, particle: ControlableParticle) +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt new file mode 100644 index 00000000..9ad00fcb --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt @@ -0,0 +1,120 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle + +/** + * `ParticleCommand` 的顺序队列:用于把多个“粒子模块/预设”组合成一个可复用的执行链。 + * + * ## 设计目的 + * 让你用非常接近 Unity 粒子系统 “模块叠加”的方式组织效果: + * - 每个 module 都是一个 `ParticleCommand` + * - 通过队列把多个 module 组装起来 + * - 每 tick 调一次 `applyVelocity(...)`,队列会按顺序把所有 module 应用到同一个粒子上 + * + * ## 典型使用方式 + * 在 `Emitter.singleParticleAction(...)` 里把队列挂到粒子 controller 的 preTick: + * ```kotlin + * val queue = ParticleCommandQueue() + * .add(AirDragCommand(0.15)) + * .add(AttractionCommand(target = center, strength = 0.8)) + * + * override fun singleParticleAction( + * controler: ParticleControler, + * data: ControlableParticleData, + * spawnPos: RelativeLocation, + * spawnWorld: Level, + * particleLerpProgress: Float, + * posLerpProgress: Float + * ) { + * controler.addPreTickAction { + * queue.applyVelocity(data, this) + * } + * } + * ``` + */ +class ParticleCommandQueue { + val commands = + ArrayDeque Boolean>>() + + @Suppress("UNCHECKED_CAST") + private val alwaysExecutePredicate = + AlwaysExecutePredicate as ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean + + /** + * 对当前粒子执行队列中的所有命令。 + * + * @param data 当前粒子的可变控制数据(被命令叠加修改) + * @param particle 当前 tick 正在更新的粒子实例(用于读取实时状态) + */ + fun applyVelocity(data: ControlableParticleData, particle: ControlableParticle) { + commands.forEach { + if (it.second(it.first, data, particle)) { + it.first.execute(data, particle) + } + } + } + + /** + * 更新队列中指定索引位置的 Command 数据。 + * + * @param T Command 的具体类型 + * @param applier 对该 Command 执行的更新逻辑(直接修改其内部参数) + * @param index Command 在队列中的索引位置 + */ + fun updateWith(index: Int, applier: T.() -> Unit): ParticleCommandQueue = + apply { + if (index !in commands.indices) { + return@apply + } + val command = commands[index].first + runCatching { applier(command as T) } + } + + /** + * 更新队列中所有符合指定类型的 Command。 + * + * @param T 需要被更新的 Command 类型 + * @param applier 对所有匹配 Command 执行的更新逻辑 + */ + inline fun updateWithTypes(applier: T.() -> Unit): ParticleCommandQueue = apply { + commands.map { it.first }.filterIsInstance() + .forEach { + applier(it) + } + } + + /** + * 向队列末尾追加一个命令,并返回自身以支持链式调用。 + * + * @param command 要追加的粒子命令 + * @return this(便于 `.add(...).add(...)` 链式写法) + */ + fun add(command: ParticleCommand): ParticleCommandQueue { + commands.add(command to alwaysExecutePredicate) + return this + } + + /** + * 向队列末尾追加一个命令,并返回自身以支持链式调用。 + * + * @param command 要追加的粒子命令 + * @param predicate 执行这个命令的条件 + * @return this(便于 `.add(...).add(...)` 链式写法) + */ + fun add( + command: ParticleCommand, + predicate: ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean + ): ParticleCommandQueue { + commands.add(command to predicate) + return this + } + + private object AlwaysExecutePredicate : Function3 { + override fun invoke( + command: ParticleCommand, + data: ControlableParticleData, + particle: Any? + ): Boolean = true + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt new file mode 100644 index 00000000..e7190ae1 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt @@ -0,0 +1,334 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.abs +import kotlin.math.floor + +class ParticleDistortionCommand() : ParticleCommand { + var center: Supplier = Supplier { Vec3.ZERO } + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + var radius: Double = 3.0 + var radialStrength: Double = 0.35 + var axialStrength: Double = 0.25 + var tangentialStrength: Double = 0.0 + var frequency: Double = 0.25 + var timeScale: Double = 0.1 + var phaseOffset: Double = 0.0 + var followStrength: Double = 0.35 + var maxStep: Double = 0.6 + var baseAxial: Double = 0.0 + var seedOffset: Int = 0 + var useLifeCurve: Boolean = false + + /** + * @param center 扰动中心点(轴线穿过的点,世界坐标)。 + * - 建议与发射器中心一致,这样“环”会被稳定地扭曲而不是整体漂移。 + * - 使用 Supplier 便于跟随移动的实体/点位(每 tick 取一次)。 + * + * @param axis 扰动轴(环所在平面的法向/法线方向)。 + * - 会自动 normalize,零向量会回退到 (0,1,0)。 + * - (0,1,0) 表示环在 XZ 平面;(1,0,0) 表示环在 YZ 平面。 + * + * @param radius 基础半径(未扰动前的目标环半径)。 + * - 设为发射器环半径时,扰动主要表现为“扭曲”而不是整体收缩/扩张。 + * - 过小会让环过紧,过大则偏离发射器形状。 + * + * @param radialStrength 径向扰动强度(沿“半径方向”的噪声幅度)。 + * - 控制“鼓包/凹陷”的程度,值越大环越不规则。 + * - 设为 0 表示不做径向扭曲,只保留其它方向扰动。 + * + * @param axialStrength 轴向扰动强度(沿“轴线方向”的噪声幅度)。 + * - 控制环在法向上的起伏(像波浪上下抖动)。 + * - 设为 0 则保持在同一平面内。 + * + * @param tangentialStrength 切向扰动强度(沿“绕环方向”的噪声幅度)。 + * - 会让粒子沿环方向滑移,产生“扭拧/拉扯”的感觉。 + * - 设为 0 可避免环周向被拉散。 + * + * @param frequency 空间频率(噪声随位置变化的密度)。 + * - 越大越“细碎/抖动”,越小越“平滑/大块”。 + * - 影响扭曲的纹理尺度。 + * + * @param timeScale 时间变化速度(噪声随时间滚动的快慢)。 + * - 0 表示静态扭曲;越大越“流动/抖动”。 + * - 过大可能出现闪烁感。 + * + * @param phaseOffset 相位偏移(用来错开不同发射器/批次的噪声)。 + * - 不改变强度,只改变噪声的“起点”。 + * + * @param followStrength 追随强度(粒子被拉回扭曲环的力度)。 + * - 越大环越“紧”,不易发散;越小越“松”,容易漂离形状。 + * + * @param maxStep 单 tick 最大修正步长(对速度增量做上限)。 + * - 防止拉回过猛导致抖动或穿透。 + * - <=0 表示不限制。 + * + * @param baseAxial 基础轴向偏移(整体沿轴线的固定偏移量)。 + * - 用于把整个环抬高/降低或制作“偏心”的扭曲面。 + * + * @param seedOffset 噪声种子偏移(同一效果的“变体”)。 + * - 相同 seedOffset 会得到相同扭曲纹理;不同值能做多种随机样式。 + * + * @param useLifeCurve 是否按生命周期衰减扰动强度。 + * - true:出生强、死亡弱,更自然。 + * - false:全生命周期强度恒定,更稳定。 + */ + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + radius: Double = 3.0, + radialStrength: Double = 0.35, + axialStrength: Double = 0.25, + tangentialStrength: Double = 0.0, + frequency: Double = 0.25, + timeScale: Double = 0.1, + phaseOffset: Double = 0.0, + followStrength: Double = 0.35, + maxStep: Double = 0.6, + baseAxial: Double = 0.0, + seedOffset: Int = 0, + useLifeCurve: Boolean = false, + ) : this() { + this.center = center + this.axis = axis + this.radius = radius + this.radialStrength = radialStrength + this.axialStrength = axialStrength + this.tangentialStrength = tangentialStrength + this.frequency = frequency + this.timeScale = timeScale + this.phaseOffset = phaseOffset + this.followStrength = followStrength + this.maxStep = maxStep + this.baseAxial = baseAxial + this.seedOffset = seedOffset + this.useLifeCurve = useLifeCurve + } + + /** + * 设置扰动中心点(轴线穿过的点,世界坐标)。 + * + * - 建议与发射器中心一致,保证环形整体不漂移。 + * - Supplier 可用于跟随移动对象,每 tick 自动读取最新位置。 + */ + fun center(v: Supplier) = apply { center = v } + + /** + * 设置扰动轴(环所在平面的法线方向)。 + * + * - 会在计算时 normalize。 + * - (0,1,0) 为 XZ 平面,(1,0,0) 为 YZ 平面。 + */ + fun axis(v: Vec3) = apply { axis = v } + + /** + * 设置基础半径(未扰动前的目标环半径)。 + * + * - 与发射器环半径一致时最稳定。 + * - 过小会让环“收紧”,过大可能偏离发射器形状。 + */ + fun radius(v: Double) = apply { radius = v } + + /** + * 设置径向扰动强度(沿半径方向的扭曲幅度)。 + * + * - 越大越“鼓包/凹陷”,环越不规则。 + * - 设为 0 可保持圆环半径稳定。 + */ + fun radialStrength(v: Double) = apply { radialStrength = v } + + /** + * 设置轴向扰动强度(沿轴线方向的起伏幅度)。 + * + * - 控制环在法向上的上下波动。 + * - 设为 0 则保持在同一平面内。 + */ + fun axialStrength(v: Double) = apply { axialStrength = v } + + /** + * 设置切向扰动强度(沿环方向的滑移幅度)。 + * + * - 让粒子沿环周向“拉扯/扭拧”。 + * - 过大可能导致环被拉散。 + */ + fun tangentialStrength(v: Double) = apply { tangentialStrength = v } + + /** + * 设置空间频率(噪声随位置变化的密度)。 + * + * - 越大越细碎、越抖;越小越平滑、越大块。 + * - 决定扭曲纹理的尺度。 + */ + fun frequency(v: Double) = apply { frequency = v } + + /** + * 设置时间变化速度(噪声滚动速度)。 + * + * - 0 表示静态扭曲。 + * - 越大越活跃,但过大容易“闪烁抖动”。 + */ + fun timeScale(v: Double) = apply { timeScale = v } + + /** + * 设置相位偏移(错开不同发射器/批次的噪声)。 + * + * - 不影响强度,只改变噪声起点。 + * - 用于避免多个环“同步抖动”。 + */ + fun phaseOffset(v: Double) = apply { phaseOffset = v } + + /** + * 设置追随强度(粒子被拉回扭曲环的力度)。 + * + * - 越大越“紧”,不易发散。 + * - 越小越“松”,允许更自由的漂移。 + */ + fun followStrength(v: Double) = apply { followStrength = v } + + /** + * 设置单 tick 最大修正步长(速度增量上限)。 + * + * - 防止拉回过猛导致抖动或穿透。 + * - <=0 表示不限制。 + */ + fun maxStep(v: Double) = apply { maxStep = v } + + /** + * 设置基础轴向偏移(整体沿轴线的固定偏移量)。 + * + * - 可把环整体抬高/降低。 + * - 配合轴向扰动可做“偏心/倾移”的扭曲环。 + */ + fun baseAxial(v: Double) = apply { baseAxial = v } + + /** + * 设置噪声种子偏移(同效果的随机变体)。 + * + * - 相同 seedOffset -> 相同扭曲纹理。 + * - 不同 seedOffset -> 不同随机外观。 + */ + fun seedOffset(v: Int) = apply { seedOffset = v } + + /** + * 设置是否按生命周期衰减扰动强度。 + * + * - true:出生强、死亡弱,更自然。 + * - false:全生命周期恒定,更稳定。 + */ + fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val centerPos = center.get() + val ax = safeNormalize(axis) + val pos = particle.loc + val r = pos.subtract(centerPos) + val axialComp = ax.scale(r.dot(ax)) + var radial = r.subtract(axialComp) + var radialLen = radial.length() + if (radialLen < 1e-9) { + radial = anyPerp(ax) + radialLen = 1.0 + } else { + radial = radial.scale(1.0 / radialLen) + } + + var tangent = ax.cross(radial) + val tLen = tangent.length() + tangent = if (tLen < 1e-9) anyPerp(ax) else tangent.scale(1.0 / tLen) + + val lifeT = if (particle.lifetime > 0) { + particle.currentAge.toDouble() / particle.lifetime.toDouble() + } else { + 0.0 + } + val lifeMul = if (useLifeCurve) (1.0 - lifeT).coerceIn(0.0, 1.0) else 1.0 + + val time = particle.currentAge.toDouble() * timeScale + phaseOffset + val local = pos.subtract(centerPos) + val p = local.scale(frequency).add(time, time * 0.7, time * 1.3) + val seed = particle.controlUUID.hashCode() + seedOffset + + val nr = noise(p, seed + 11) + val na = noise(p, seed + 23) + val nt = noise(p, seed + 37) + + val targetRadius = (radius + nr * radialStrength * lifeMul).coerceAtLeast(0.0) + val targetAxial = baseAxial + na * axialStrength * lifeMul + val targetTangential = nt * tangentialStrength * lifeMul + + val targetPos = centerPos + .add(ax.scale(targetAxial)) + .add(radial.scale(targetRadius)) + .add(tangent.scale(targetTangential)) + + var dv = targetPos.subtract(pos).scale(followStrength) + val dvLen = dv.length() + if (maxStep > 0.0 && dvLen > maxStep) { + dv = dv.scale(maxStep / dvLen) + } + data.velocity = data.velocity.add(dv) + } + + private fun safeNormalize(v: Vec3): Vec3 { + val len = v.length() + return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) + } + + private fun anyPerp(axis: Vec3): Vec3 { + val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) + val perp = axis.cross(ref) + val len = perp.length() + return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) + } + + private fun noise(p: Vec3, seed: Int): Double { + return valueNoise3(p, seed) * 2.0 - 1.0 + } + + private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) + private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t + + private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { + var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 + n = (n xor (n ushr 13)) * 1274126177 + n = n xor (n ushr 16) + return (n and 0x7fffffff).toDouble() / 2147483647.0 + } + + private fun valueNoise3(p: Vec3, seed: Int): Double { + val x0 = floor(p.x).toInt() + val y0 = floor(p.y).toInt() + val z0 = floor(p.z).toInt() + val fx = p.x - x0 + val fy = p.y - y0 + val fz = p.z - z0 + + val u = fade(fx) + val v = fade(fy) + val w = fade(fz) + + fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) + + val n000 = h(0, 0, 0) + val n100 = h(1, 0, 0) + val n010 = h(0, 1, 0) + val n110 = h(1, 1, 0) + val n001 = h(0, 0, 1) + val n101 = h(1, 0, 1) + val n011 = h(0, 1, 1) + val n111 = h(1, 1, 1) + + val nx00 = lerp(n000, n100, u) + val nx10 = lerp(n010, n110, u) + val nx01 = lerp(n001, n101, u) + val nx11 = lerp(n011, n111, u) + + val nxy0 = lerp(nx00, nx10, v) + val nxy1 = lerp(nx01, nx11, v) + + return lerp(nxy0, nxy1, w) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt new file mode 100644 index 00000000..e908304b --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt @@ -0,0 +1,99 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 + +/** + * 空气阻尼(Drag) + * + * 每 tick 把速度乘上 (1 - drag),让粒子慢下来、更“黏”。 + */ +class ParticleDragCommand() : ParticleCommand { + /** + * 阻尼强度(>=0)。 + * + * - 0.0:完全不阻尼 + * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) + * - 0.2 ~ 1.0:明显刹车(碎屑、火花) + * - >1.0:非常快停(像在胶水里) + */ + var damping: Double = 0.15 + + /** + * 最小速度阈值(<=0 表示不使用)。 + * + * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 + */ + var minSpeed: Double = 0.0 + + /** + * 额外的线性阻力(每 tick 额外减去 speed * linear) + * + * - 推荐保持 0,除非你想要“低速更粘”的感觉 + */ + var linear: Double = 0.0 + + /** + * @param damping 阻尼强度(>=0),越大速度衰减越快 + * @param minSpeed 低于该速度则直接归零(<=0 不启用) + * @param linear 线性阻力项(建议 0) + */ + constructor( + damping: Double = 0.15, + minSpeed: Double = 0.0, + linear: Double = 0.0, + ) : this() { + this.damping = damping + this.minSpeed = minSpeed + this.linear = linear + } + + /** + * 阻尼强度(>=0)。 + * + * - 0.0:完全不阻尼 + * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) + * - 0.2 ~ 1.0:明显刹车(碎屑、火花) + * - >1.0:非常快停(像在胶水里) + */ + fun damping(v: Double) = apply { damping = v } + + /** + * 最小速度阈值(<=0 表示不使用)。 + * + * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 + */ + fun minSpeed(v: Double) = apply { minSpeed = v } + + /** + * 额外的线性阻力(每 tick 额外减去 speed * linear) + * + * - 推荐保持 0,除非你想要“低速更粘”的感觉 + */ + fun linear(v: Double) = apply { linear = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + var v = data.velocity + val speed = v.length() + if (minSpeed > 0 && speed <= minSpeed) { + data.velocity = Vec3.ZERO + return + } + + // exp damping + val factor = GraphMathHelper.expDampFactor(damping, 1.0) + v = v.scale(factor) + + // optional linear drag + if (linear > 0.0) { + val s2 = v.length() + if (s2 > 1e-9) { + val shrink = (1.0 - linear).coerceIn(0.0, 1.0) + v = v.scale(shrink) + } + } + data.velocity = v + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt new file mode 100644 index 00000000..b50e6fb9 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt @@ -0,0 +1,119 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import kotlin.math.cos +import kotlin.math.sin + +/** + * 流场:根据粒子位置/时间生成一个“风向量”,并叠加到速度里。 + * + * 这个实现是解析型流场(sin/cos),特点: + * - 计算非常快 + * - 通过 frequency / amplitude / timeScale 能做出很多“丝带/涡流”效果 + */ +class ParticleFlowFieldCommand() : ParticleCommand { + + /** + * 振幅(每 tick 的速度增量幅度) + * + * - 0.01 ~ 0.1:微风(烟雾漂移) + * - 0.1 ~ 0.8:明显流动(能看出纹理) + * - >0.8:很强(容易乱飞,需要阻尼/限速) + */ + var amplitude: Double = 0.15 + + /** + * 空间频率(越大纹理越密,越小越平缓) + * + * - 0.05 ~ 0.2:大尺度流场 + * - 0.2 ~ 1.0:中小尺度卷曲 + */ + var frequency: Double = 0.25 + + /** + * 时间缩放(控制流场随时间变化速度) + * + * - 0:静态流场(像固定风) + * - 0.02 ~ 0.2:缓慢变动 + * - >0.3:快速抖动/波动 + */ + var timeScale: Double = 0.06 + + /** + * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) + */ + var phaseOffset: Double = 0.0 + + /** + * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) + */ + var worldOffset: Vec3 = Vec3.ZERO + + constructor( + amplitude: Double = 0.15, + frequency: Double = 0.25, + timeScale: Double = 0.06, + phaseOffset: Double = 0.0, + worldOffset: Vec3 = Vec3.ZERO, + ) : this() { + this.amplitude = amplitude + this.frequency = frequency + this.timeScale = timeScale + this.phaseOffset = phaseOffset + this.worldOffset = worldOffset + } + + /** + * 振幅(每 tick 的速度增量幅度) + * + * - 0.01 ~ 0.1:微风(烟雾漂移) + * - 0.1 ~ 0.8:明显流动(能看出纹理) + * - >0.8:很强(容易乱飞,需要阻尼/限速) + */ + fun amplitude(v: Double) = apply { amplitude = v } + + /** + * 空间频率(越大纹理越密,越小越平缓) + * + * - 0.05 ~ 0.2:大尺度流场 + * - 0.2 ~ 1.0:中小尺度卷曲 + */ + fun frequency(v: Double) = apply { frequency = v } + + /** + * 时间缩放(控制流场随时间变化速度) + * + * - 0:静态流场(像固定风) + * - 0.02 ~ 0.2:缓慢变动 + * - >0.3:快速抖动/波动 + */ + fun timeScale(v: Double) = apply { timeScale = v } + + /** + * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) + */ + fun phaseOffset(v: Double) = apply { phaseOffset = v } + + /** + * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) + */ + fun worldOffset(v: Vec3) = apply { worldOffset = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val p = particle.loc.add(worldOffset) + val t = (particle.currentAge.toDouble() * timeScale) + phaseOffset + + // A cheap "curl-ish" analytic field + val fx = sin((p.y + t) * frequency) + cos((p.z - t) * frequency) + val fy = sin((p.z + t) * frequency) + cos((p.x + t) * frequency) + val fz = sin((p.x - t) * frequency) + cos((p.y - t) * frequency) + + // normalize-ish (avoid sqrt cost, keep stable magnitude) + val scale = 0.5 + val dv = Vec3(fx * scale, fy * scale, fz * scale).scale(amplitude) + + data.velocity = data.velocity.add(dv) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt new file mode 100644 index 00000000..7b73bd04 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt @@ -0,0 +1,20 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.particles.ControlableParticle + +/** + * 在emitter内部使用updatePhysics 或者使用这个 + * + * @property emitter 对应粒子发射器 + */ +class ParticleGravityCommand(val emitter: ClassParticleEmitters) : ParticleCommand { + override fun execute( + data: ControlableParticleData, + particle: ControlableParticle + ) { + data.velocity = data.velocity.add(0.0, -emitter.gravity, 0.0) + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt new file mode 100644 index 00000000..7dd6b709 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class ParticleInheritMode { + INITIAL, + CURRENT +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt new file mode 100644 index 00000000..8c80e5e4 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt @@ -0,0 +1,340 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector3f +import java.util.function.Supplier +import kotlin.random.Random + +/** + * 继承速度命令(Inherit Velocity)。 + * + * 目标: + * - 让粒子继承“发射源速度”或任意自定义速度源 + * - 支持一次性继承(INITIAL)和持续跟随(CURRENT) + * - 支持按轴屏蔽、生命周期权重、空间转换、阻尼跟随 + * + * 常见场景: + * - 武器挥动拖尾:CURRENT + 适度 damping + * - 喷射火花:INITIAL + 较高 multiplier + */ +class ParticleInheritVelocityCommand() : ParticleCommand { + /** + * 速度来源。 + * + * 默认返回 `Vec3.ZERO`,通常你会传入: + * - `Supplier { emitter.emitterVelocity }` + * - 或任意自定义动态速度源 + */ + var source: Supplier = Supplier { Vec3.ZERO } + + /** + * 继承模式。 + * + * - INITIAL:只在首次执行时继承,后续保持(或按 damping 衰减) + * - CURRENT:每 tick 跟随最新来源速度 + */ + var mode: ParticleInheritMode = ParticleInheritMode.INITIAL + + /** + * 继承倍率。 + * + * - 1.0:等于来源速度 + * - <1.0:弱继承 + * - >1.0:强化继承 + */ + var multiplier: Double = 1.0 + + /** + * 轴向掩码(逐轴相乘)。 + * + * 示例: + * - (1,0,1):忽略 Y 轴继承 + * - (0,1,0):只继承竖直方向 + */ + var axisMask: Vec3 = Vec3(1.0, 1.0, 1.0) + + /** + * 生命周期权重曲线。 + * + * 采样值会乘到继承速度上: + * - 1 -> 全量继承 + * - 0 -> 不继承 + */ + var overLifetime: FloatCurve = ConstantFloatCurve(1.0) + + /** + * 阻尼强度(用于平滑/衰减继承速度)。 + * + * - 0:不平滑,立即跟随 + * - >0:指数平滑,值越大变化越柔和 + */ + var damping: Double = 0.0 + + /** + * 继承速度长度上限。 + * + * - <=0:不限制 + * - >0:超过则截断 + */ + var maxContributionSpeed: Double = 0.0 + + /** + * 继承向量所在空间。 + * + * - WORLD:按世界轴解释 + * - LOCAL:按粒子当前旋转解释后再转到世界空间 + */ + var space: ParticleMotionSpace = ParticleMotionSpace.WORLD + + /** + * 是否启用按粒子固定随机缩放。 + */ + var randomizePerParticle: Boolean = false + + /** + * 随机缩放最小值(按轴独立采样)。 + */ + var randomScaleMin: Double = 1.0 + + /** + * 随机缩放最大值(按轴独立采样)。 + */ + var randomScaleMax: Double = 1.0 + + /** + * 随机种子偏移。 + */ + var randomSeedOffset: Int = 0 + + private val stateKey = "inherit_velocity.state.${System.identityHashCode(this)}" + private val randomScaleKey = "inherit_velocity.random_scale.${System.identityHashCode(this)}" + + private data class InheritState( + var initialized: Boolean = false, + var applied: Vec3 = Vec3.ZERO + ) + + private data class AxisRandomScale( + val x: Double, + val y: Double, + val z: Double + ) + + /** + * @param source 速度来源 + * @param mode 继承模式(INITIAL/CURRENT) + * @param multiplier 继承倍率 + * @param axisMask 轴向掩码 + * @param overLifetime 生命周期权重曲线 + * @param damping 平滑阻尼 + * @param maxContributionSpeed 继承速度上限 + * @param space 继承空间(WORLD/LOCAL) + */ + constructor( + source: Supplier, + mode: ParticleInheritMode = ParticleInheritMode.INITIAL, + multiplier: Double = 1.0, + axisMask: Vec3 = Vec3(1.0, 1.0, 1.0), + overLifetime: FloatCurve = ConstantFloatCurve(1.0), + damping: Double = 0.0, + maxContributionSpeed: Double = 0.0, + space: ParticleMotionSpace = ParticleMotionSpace.WORLD + ) : this() { + this.source = source + this.mode = mode + this.multiplier = multiplier + this.axisMask = axisMask + this.overLifetime = overLifetime + this.damping = damping + this.maxContributionSpeed = maxContributionSpeed + this.space = space + } + + /** + * 便捷构造:直接继承指定发射器的每 tick 位移速度。 + */ + constructor(emitter: ClassParticleEmitters) : this( + source = Supplier { emitter.emitterVelocity } + ) + + /** + * 设置速度来源。 + */ + fun source(v: Supplier) = apply { source = v } + + /** + * 设置继承模式。 + * + * - INITIAL:一次继承 + * - CURRENT:持续跟随 + */ + fun mode(v: ParticleInheritMode) = apply { mode = v } + + /** + * 设置继承倍率。 + */ + fun multiplier(v: Double) = apply { multiplier = v } + + /** + * 设置轴向掩码。 + */ + fun axisMask(v: Vec3) = apply { axisMask = v } + + /** + * 设置生命周期权重曲线。 + */ + fun overLifetime(v: FloatCurve) = apply { overLifetime = v } + + /** + * 设置阻尼强度。 + */ + fun damping(v: Double) = apply { damping = v } + + /** + * 设置继承速度上限。 + */ + fun maxContributionSpeed(v: Double) = apply { maxContributionSpeed = v } + + /** + * 设置继承空间(WORLD/LOCAL)。 + */ + fun space(v: ParticleMotionSpace) = apply { space = v } + + /** + * 设置是否启用按粒子固定随机缩放。 + */ + fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } + + /** + * 设置随机缩放区间。 + */ + fun randomScale(min: Double, max: Double) = apply { + randomScaleMin = min + randomScaleMax = max + } + + /** + * 设置随机种子偏移。 + */ + fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } + + /** + * 每 tick 应用继承速度。 + */ + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val state = particle.controler.bufferedData.getOrPut(stateKey) { InheritState() } as InheritState + + val t = normalizedLifetime(particle) + val lifeMultiplier = overLifetime.sample(t) + val randomScale = resolveRandomScale(particle) + + var target = source.get() + target = applyMask(target) + target = scaleAxis(target, randomScale) + target = target.scale(multiplier * lifeMultiplier) + target = transformBySpace(target, space, particle) + target = clampLength(target, maxContributionSpeed) + + target = when (mode) { + ParticleInheritMode.INITIAL -> { + if (!state.initialized) { + state.initialized = true + target + } else if (damping > 0.0) { + state.applied.scale(GraphMathHelper.expDampFactor(damping, 1.0)) + } else { + state.applied + } + } + + ParticleInheritMode.CURRENT -> { + state.initialized = true + if (damping > 0.0) { + val follow = 1.0 - GraphMathHelper.expDampFactor(damping, 1.0) + state.applied.add(target.subtract(state.applied).scale(follow.coerceIn(0.0, 1.0))) + } else { + target + } + } + } + + val delta = target.subtract(state.applied) + if (delta.lengthSqr() > 1e-12) { + data.velocity = data.velocity.add(delta) + } + state.applied = target + } + + private fun applyMask(vec: Vec3): Vec3 { + return Vec3( + vec.x * axisMask.x, + vec.y * axisMask.y, + vec.z * axisMask.z + ) + } + + private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { + return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) + } + + private fun normalizedLifetime(particle: ControlableParticle): Double { + if (particle.lifetime <= 0) return 0.0 + return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) + } + + private fun transformBySpace( + vec: Vec3, + space: ParticleMotionSpace, + particle: ControlableParticle + ): Vec3 { + if (space == ParticleMotionSpace.WORLD) return vec + + val q = Quaternionf().rotateXYZ( + particle.currentPitch, + particle.currentYaw, + particle.currentRoll + ) + val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) + v.rotate(q) + return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) + } + + private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { + if (!randomizePerParticle) { + return AxisRandomScale(1.0, 1.0, 1.0) + } + val cached = particle.controler.bufferedData[randomScaleKey] + if (cached is AxisRandomScale) { + return cached + } + + val minScale = minOf(randomScaleMin, randomScaleMax) + val maxScale = maxOf(randomScaleMin, randomScaleMax) + val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) + val fixed = if (maxScale - minScale <= 1e-9) { + AxisRandomScale(minScale, minScale, minScale) + } else { + AxisRandomScale( + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale) + ) + } + particle.controler.bufferedData[randomScaleKey] = fixed + return fixed + } + + private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { + if (maxLen <= 0.0) return vec + val len = vec.length() + if (len <= 1e-9 || len <= maxLen) return vec + return vec.scale(maxLen / len) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt new file mode 100644 index 00000000..1fb7be06 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt @@ -0,0 +1,377 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve +import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector3f +import kotlin.random.Random + +/** + * 生命周期运动命令(Force over Lifetime + Velocity over Lifetime)。 + * + * 你可以把它理解为 Unity 里两个模块的组合版: + * - Force over Lifetime:每 tick 给 `data.velocity` 增加一段加速度 + * - Velocity over Lifetime:每 tick 按模式塑形速度(叠加/覆盖/乘法) + * + * 执行流程: + * 1. 计算生命周期进度 `t = currentAge / lifetime` + * 2. 采样 Force 曲线并叠加到当前速度 + * 3. 采样 Velocity 曲线并按 `velocityMode` 生效 + * 4. 可选:应用按粒子固定随机缩放(提升群体离散感) + */ +class ParticleLifetimeMotionCommand() : ParticleCommand { + /** + * Force 曲线 X 分量(每 tick 对 velocity.x 的增量)。 + * + * - 正值:向世界/局部 +X 推 + * - 负值:向世界/局部 -X 推 + */ + var forceX: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Force 曲线 Y 分量(每 tick 对 velocity.y 的增量)。 + * + * 常用于: + * - 上升烟雾:前期正值,后期逐渐趋近 0 + * - 下坠火星:前期小正值,后期转负值 + */ + var forceY: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Force 曲线 Z 分量(每 tick 对 velocity.z 的增量)。 + */ + var forceZ: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Velocity 曲线 X 分量。 + * + * 实际如何生效取决于 `velocityMode`: + * - ADD:视作附加速度曲线 + * - OVERRIDE:视作目标速度 + * - MULTIPLY:视作按轴倍率 + */ + var velocityX: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Velocity 曲线 Y 分量。 + */ + var velocityY: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Velocity 曲线 Z 分量。 + */ + var velocityZ: FloatCurve = ConstantFloatCurve(0.0) + + /** + * Force 曲线所在空间。 + * + * - WORLD:曲线向量直接当世界方向 + * - LOCAL:曲线向量会按粒子当前 pitch/yaw/roll 旋转到世界方向 + */ + var forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD + + /** + * Velocity 曲线所在空间。 + * + * - WORLD:速度塑形不随粒子朝向变化 + * - LOCAL:速度塑形会跟随粒子当前朝向 + */ + var velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD + + /** + * Velocity 曲线作用模式。 + * + * - ADD:把曲线速度作为“附加项”并做增量追踪 + * - OVERRIDE:把当前速度拉向曲线速度 + * - MULTIPLY:按轴对当前速度做乘法 + */ + var velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD + + /** + * 是否启用“按粒子固定随机缩放”。 + * + * 开启后每个粒子只在首次执行时采样一次随机倍率, + * 后续 tick 重用同一倍率,保证轨迹连贯。 + */ + var randomizePerParticle: Boolean = false + + /** + * 随机缩放最小值(按轴独立采样)。 + */ + var randomScaleMin: Double = 1.0 + + /** + * 随机缩放最大值(按轴独立采样)。 + */ + var randomScaleMax: Double = 1.0 + + /** + * 随机种子偏移。 + * + * 在相同参数下改变该值,可快速得到另一套离散分布。 + */ + var randomSeedOffset: Int = 0 + + /** + * 限制 Velocity over Lifetime 在单 tick 的最大速度改变量。 + * + * - <= 0:不限制 + * - > 0:超过该值会被裁剪 + * + * 作用:抑制曲线突变导致的速度跳变或爆速。 + */ + var maxVelocityDeltaPerTick: Double = 0.0 + + private val velocityStateKey = "lifetime_motion.velocity_state.${System.identityHashCode(this)}" + private val randomScaleKey = "lifetime_motion.random_scale.${System.identityHashCode(this)}" + + private data class VelocityState(var appliedVelocity: Vec3 = Vec3.ZERO) + private data class AxisRandomScale(val x: Double, val y: Double, val z: Double) + + /** + * @param forceX Force 曲线 X + * @param forceY Force 曲线 Y + * @param forceZ Force 曲线 Z + * @param velocityX Velocity 曲线 X + * @param velocityY Velocity 曲线 Y + * @param velocityZ Velocity 曲线 Z + * @param forceSpace Force 所在空间 + * @param velocitySpace Velocity 所在空间 + * @param velocityMode Velocity 生效模式 + */ + constructor( + forceX: FloatCurve = ConstantFloatCurve(0.0), + forceY: FloatCurve = ConstantFloatCurve(0.0), + forceZ: FloatCurve = ConstantFloatCurve(0.0), + velocityX: FloatCurve = ConstantFloatCurve(0.0), + velocityY: FloatCurve = ConstantFloatCurve(0.0), + velocityZ: FloatCurve = ConstantFloatCurve(0.0), + forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, + velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, + velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD + ) : this() { + this.forceX = forceX + this.forceY = forceY + this.forceZ = forceZ + this.velocityX = velocityX + this.velocityY = velocityY + this.velocityZ = velocityZ + this.forceSpace = forceSpace + this.velocitySpace = velocitySpace + this.velocityMode = velocityMode + } + + /** + * 一次性设置 Force 曲线 XYZ。 + * + * 示例: + * - `x=0,y>0,z=0`:纯上升 + * - `x/z 对称`:横向扩散 + */ + fun forceCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { + forceX = x + forceY = y + forceZ = z + } + + /** + * 一次性设置 Velocity 曲线 XYZ。 + * + * 建议: + * - 做“轨道化速度”用 `OVERRIDE` + * - 做“微调补偿”用 `ADD` + */ + fun velocityCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { + velocityX = x + velocityY = y + velocityZ = z + } + + /** + * 设置 Force 曲线空间。 + * + * - WORLD:固定世界方向 + * - LOCAL:跟随粒子朝向 + */ + fun forceSpace(v: ParticleMotionSpace) = apply { forceSpace = v } + + /** + * 设置 Velocity 曲线空间。 + * + * - WORLD:常用于全局风场/轨道 + * - LOCAL:常用于“朝前推进”类效果 + */ + fun velocitySpace(v: ParticleMotionSpace) = apply { velocitySpace = v } + + /** + * 设置 Velocity 曲线作用模式。 + * + * - ADD / OVERRIDE / MULTIPLY + */ + fun velocityMode(v: ParticleLifetimeVelocityMode) = apply { velocityMode = v } + + /** + * 设置是否启用按粒子固定随机缩放。 + */ + fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } + + /** + * 设置随机缩放区间。 + * + * - 当 `min == max` 时等价于固定倍率 + * - 区间越大,离散感越强,但整体可控性会下降 + */ + fun randomScale(min: Double, max: Double) = apply { + randomScaleMin = min + randomScaleMax = max + } + + /** + * 设置随机种子偏移。 + */ + fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } + + /** + * 设置 Velocity 曲线每 tick 最大速度改变量。 + */ + fun maxVelocityDeltaPerTick(v: Double) = apply { maxVelocityDeltaPerTick = v } + + /** + * 每 tick 应用生命周期运动。 + */ + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val t = normalizedLifetime(particle) + val randomScale = resolveRandomScale(particle) + + val force = transformBySpace( + scaleAxis(sampleForce(t), randomScale), + forceSpace, + particle + ) + if (force.lengthSqr() > 1e-12) { + data.velocity = data.velocity.add(force) + } + + val sampledVelocity = transformBySpace( + scaleAxis(sampleVelocity(t), randomScale), + velocitySpace, + particle + ) + when (velocityMode) { + ParticleLifetimeVelocityMode.ADD -> applyVelocityAdditive(data, particle, sampledVelocity) + ParticleLifetimeVelocityMode.OVERRIDE -> { + particle.controler.bufferedData.remove(velocityStateKey) + var target = sampledVelocity + if (maxVelocityDeltaPerTick > 0.0) { + val delta = clampLength(target.subtract(data.velocity), maxVelocityDeltaPerTick) + target = data.velocity.add(delta) + } + data.velocity = target + } + + ParticleLifetimeVelocityMode.MULTIPLY -> { + particle.controler.bufferedData.remove(velocityStateKey) + val current = data.velocity + var target = Vec3( + current.x * sampledVelocity.x, + current.y * sampledVelocity.y, + current.z * sampledVelocity.z + ) + if (maxVelocityDeltaPerTick > 0.0) { + val delta = clampLength(target.subtract(current), maxVelocityDeltaPerTick) + target = current.add(delta) + } + data.velocity = target + } + } + } + + private fun applyVelocityAdditive( + data: ControlableParticleData, + particle: ControlableParticle, + sampledVelocity: Vec3 + ) { + val state = particle.controler.bufferedData.getOrPut(velocityStateKey) { VelocityState() } as VelocityState + val delta = clampLength(sampledVelocity.subtract(state.appliedVelocity), maxVelocityDeltaPerTick) + if (delta.lengthSqr() > 1e-12) { + data.velocity = data.velocity.add(delta) + } + state.appliedVelocity = state.appliedVelocity.add(delta) + } + + private fun sampleForce(t: Double): Vec3 { + return Vec3( + forceX.sample(t), + forceY.sample(t), + forceZ.sample(t) + ) + } + + private fun sampleVelocity(t: Double): Vec3 { + return Vec3( + velocityX.sample(t), + velocityY.sample(t), + velocityZ.sample(t) + ) + } + + private fun normalizedLifetime(particle: ControlableParticle): Double { + if (particle.lifetime <= 0) return 0.0 + return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) + } + + private fun transformBySpace( + vec: Vec3, + space: ParticleMotionSpace, + particle: ControlableParticle + ): Vec3 { + if (space == ParticleMotionSpace.WORLD) return vec + + val q = Quaternionf().rotateXYZ( + particle.currentPitch, + particle.currentYaw, + particle.currentRoll + ) + val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) + v.rotate(q) + return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) + } + + private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { + return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) + } + + private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { + if (!randomizePerParticle) { + return AxisRandomScale(1.0, 1.0, 1.0) + } + val cached = particle.controler.bufferedData[randomScaleKey] + if (cached is AxisRandomScale) { + return cached + } + val minScale = minOf(randomScaleMin, randomScaleMax) + val maxScale = maxOf(randomScaleMin, randomScaleMax) + val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) + val fixed = if (maxScale - minScale <= 1e-9) { + AxisRandomScale(minScale, minScale, minScale) + } else { + AxisRandomScale( + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale), + random.nextDouble(minScale, maxScale) + ) + } + particle.controler.bufferedData[randomScaleKey] = fixed + return fixed + } + + private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { + if (maxLen <= 0.0) return vec + val len = vec.length() + if (len <= 1e-9 || len <= maxLen) return vec + return vec.scale(maxLen / len) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt new file mode 100644 index 00000000..c903b551 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt @@ -0,0 +1,7 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class ParticleLifetimeVelocityMode { + ADD, + OVERRIDE, + MULTIPLY +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt new file mode 100644 index 00000000..69a4d496 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +enum class ParticleMotionSpace { + WORLD, + LOCAL +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt new file mode 100644 index 00000000..9486f3fa --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt @@ -0,0 +1,205 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.extend.multiply +import cn.coostack.cooparticlesapi.extend.times +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import kotlin.math.floor + +/** + * 噪声扰动(Noise / Turbulence) + * + * 作用:在每个 tick 给粒子速度叠加一股“连续的风”,让轨迹更自然、更有生命感, + * 常用于:烟雾/火焰抖动、能量流扰动、爆炸尘埃乱流、魔法粒子“活跃感”。 + * + * @property strength + * - 噪声加速度强度(每 tick 叠加到 velocity 的幅度) + * - 越大:风越“猛”,轨迹更弯、更乱、更炸;过大时会出现抽搐/抖动、偏航严重、粒子群散架。 + * - 越小:扰动更轻微,只是细腻的呼吸感/轻微摆动,整体运动仍主要由初速度决定。 + * - 经验:火焰/烟雾中等,能量微扰偏小,爆炸尘埃可偏大但必须配合 drag 或 clampSpeed。 + * + * @property frequency + * - 空间频率(噪声随“空间位置”变化的密度,决定扰动“颗粒感”) + * - 越大:噪声变化更快、更碎、更“抖”,局部细节更丰富;过大时会像高频抖动/电流乱颤。 + * - 越小:噪声变化更慢、更平滑,形成大尺度的“风带/云团”漂移;过小会显得太温柔、缺细节。 + * - 直觉:frequency 决定“风场纹理”的粗细(大=细碎,小=粗大块)。 + * + * @property speed + * - 时间滚动速度(噪声场随时间流动的快慢) + * - 越大:风场变化更快,扰动更活跃,像湍急的气流;过大时会像噪声在闪烁,产生“乱抖”的观感。 + * - 越小:风场变化更慢,像缓慢流动的空气/云层;过小会显得“静止抖动”,缺少流体感。 + * - 直觉:speed 决定“风在动得有多快”(不是风的力量,力量是 strength)。 + * + * @property affectY + * - 垂直影响比例(对 Y 方向扰动的缩放) + * - 越大:上下翻滚更明显,更“蓬/炸”,适合爆炸烟尘、魔法能量上涌;过大会显得漂浮、失重。 + * - 越小:更贴地/更平,垂直更稳定;常用于烟雾(避免上下乱跳)或贴地尘土。 + * - 常用:烟雾 0.3~0.7;能量上涌可 1.0~1.5;贴地尘土可 0.0~0.3。 + * + * @property clampSpeed + * - 限速(对最终速度做上限,防止速度叠加越来越快) + * - 越大:允许粒子跑得更快,扰动会更“飘逸/发散”;过大时可能被 strength 一直堆出很高速度。 + * - 越小:更“黏”,粒子速度被压住,群体更稳更聚;过小时会把扰动的动态感也一起压扁。 + * - 直觉:clampSpeed 是“安全阀/天花板”,用来保证效果不会失控。 + * + * @property useLifeCurve + * - 是否按生命周期调制强度(把 strength 乘上寿命曲线) + * - true:扰动会随生命周期变化(常见:出生强→死亡弱,或先弱后强),更像真实流体/能量变化。 + * - false:全程扰动强度恒定,更稳定可控,但可能显得“机械”,不够有呼吸感。 + * - 建议:大部分效果都开 true;只有想要“持续稳定噪声”时才关(例如长时间环境粒子)。 + */ +class ParticleNoiseCommand( + private var strength: Double = 0.03, + private var frequency: Double = 0.15, + private var speed: Double = 0.12, + private var affectY: Double = 1.0, + private var clampSpeed: Double = 0.8, + private var useLifeCurve: Boolean = true, +) : ParticleCommand { + constructor() : this(0.03, 0.15, 0.12, 1.0, 0.8, true) + + /** + * 设置噪声加速度强度(每 tick 叠加到 velocity 的幅度)。 + * + * - 越大:扰动更猛,轨迹更弯、更乱,需注意配合限速或阻尼 + * - 越小:扰动更轻微,仅提供细腻的呼吸感 + */ + fun strength(v: Double) = apply { strength = v } + + /** + * 设置空间频率(噪声随空间位置变化的密度)。 + * + * - 越大:扰动更细碎、更抖动 + * - 越小:扰动更平滑,形成大尺度风带 + */ + fun frequency(v: Double) = apply { frequency = v } + + /** + * 设置时间滚动速度(噪声场随时间变化的快慢)。 + * + * - 越大:风场变化更快,更活跃 + * - 越小:风场变化更慢,更沉稳 + */ + fun speed(v: Double) = apply { speed = v } + + /** + * 设置垂直方向(Y 轴)的扰动影响比例。 + * + * - 越大:上下翻滚更明显,更蓬松 + * - 越小:垂直更稳定,更贴地 + */ + fun affectY(v: Double) = apply { affectY = v } + + /** + * 设置速度上限(最终 velocity 的最大长度)。 + * + * - 用于防止噪声扰动持续叠加导致速度失控 + * - 值越大:运动更飘逸、发散 + * - 值越小:粒子群更稳、更聚 + */ + fun clampSpeed(v: Double) = apply { clampSpeed = v } + + /** + * 设置是否按生命周期曲线调制扰动强度。 + * + * - true:扰动随粒子生命周期变化,更自然、更有呼吸感 + * - false:全程恒定扰动,更稳定、可控 + */ + fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } + + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + // 0..1 生命周期进度 + val t = if (particle.lifetime > 0) particle.currentAge.toDouble() / particle.lifetime.toDouble() else 0.0 + + // 取粒子的稳定 seed(一定要稳定) + // 你可以换成 data.seed / spawnIndex / uuid hash + val seed = particle.controlUUID.hashCode() // 如果没有 id,自己做一个 + + // 时间输入:连续滚动 + val time = (particle.currentAge.toDouble()) * speed + val pos = particle.loc + // 噪声采样点:位置 * 频率 + time + val p = (Vec3(pos.x, pos.y, pos.z) * this.frequency) + .add(time, time * 0.7, time * 1.3) + + var amp = strength + if (useLifeCurve) { + // 这里你换成 data 的曲线:例如 data.strengthCurve.sample(t) + // 我给默认:前强后弱(像烟) + val lifeMul = (1.0 - t) + amp *= lifeMul + } + + val n = noiseVec3(p, seed) + val dv = Vec3(n.x, n.y * affectY, n.z).multiply(amp) + + val v0 = data.velocity + var v = v0.add(dv) + + // 限速(很重要) + val sp2 = v.lengthSqr() + val max2 = clampSpeed * clampSpeed + if (sp2 > max2) { + v = v.normalize().multiply(clampSpeed) + } + + data.velocity = v + } + + private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) + private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t + + private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { + // 纯 hash -> 0..1 + var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 + n = (n xor (n ushr 13)) * 1274126177 + n = n xor (n ushr 16) + // 转成 0..1 + return ((n and 0x7fffffff).toDouble() / 2147483647.0) + } + + private fun valueNoise3(p: Vec3, seed: Int): Double { + val x0 = floor(p.x).toInt() + val y0 = floor(p.y).toInt() + val z0 = floor(p.z).toInt() + val fx = p.x - x0 + val fy = p.y - y0 + val fz = p.z - z0 + + val u = fade(fx) + val v = fade(fy) + val w = fade(fz) + + fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) + + val n000 = h(0, 0, 0) + val n100 = h(1, 0, 0) + val n010 = h(0, 1, 0) + val n110 = h(1, 1, 0) + val n001 = h(0, 0, 1) + val n101 = h(1, 0, 1) + val n011 = h(0, 1, 1) + val n111 = h(1, 1, 1) + + val nx00 = lerp(n000, n100, u) + val nx10 = lerp(n010, n110, u) + val nx01 = lerp(n001, n101, u) + val nx11 = lerp(n011, n111, u) + + val nxy0 = lerp(nx00, nx10, v) + val nxy1 = lerp(nx01, nx11, v) + + return lerp(nxy0, nxy1, w) // 0..1 + } + + private fun noiseVec3(pos: Vec3, seed: Int): Vec3 { + // 用不同 seed 偏移得到 xyz 三通道 + val nx = valueNoise3(pos, seed + 11) * 2 - 1 + val ny = valueNoise3(pos, seed + 23) * 2 - 1 + val nz = valueNoise3(pos, seed + 37) * 2 - 1 + val v = Vec3(nx, ny, nz) + return if (v.lengthSqr() < 1e-8) Vec3.ZERO else v.normalize() + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt new file mode 100644 index 00000000..858fef4c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt @@ -0,0 +1,177 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.max + +class ParticleOrbitCommand() : ParticleCommand { + /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 目标轨道半径(世界单位) + * + * - 值越小:轨道越紧凑 + * - 值越大:粒子绕行范围越大 + */ + var radius: Double = 3.0 + + /** + * 角速度强度(每 tick 施加的切向速度增量) + * + * - 较小:缓慢绕行 + * - 较大:快速旋转(建议配合限速或阻尼) + */ + var angularSpeed: Double = 0.35 + + /** + * 径向纠正强度(用于将粒子拉回目标半径) + * + * - 值越小:允许轨道半径有较大漂移 + * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 + */ + var radialCorrect: Double = 0.25 + + /** + * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) + */ + var minDistance: Double = 0.2 + + /** + * 轨道模式 + * + * - PHYSICAL:真实物理(默认) + * - SPRING:弹簧收敛(推荐通用) + * - SNAP:强制吸附(最稳定) + */ + var mode: OrbitMode = OrbitMode.PHYSICAL + + /** + * 仅在 PHYSICAL / SPRING 下生效: + * 每 tick 最大径向修正量(防止启动时被拉飞) + */ + var maxRadialStep: Double = 0.5 + + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + radius: Double = 3.0, + angularSpeed: Double = 0.35, + radialCorrect: Double = 0.25, + minDistance: Double = 0.2, + mode: OrbitMode = OrbitMode.PHYSICAL, + ) : this() { + this.center = center + this.axis = axis + this.radius = radius + this.angularSpeed = angularSpeed + this.radialCorrect = radialCorrect + this.minDistance = minDistance + this.mode = mode + } + + /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ + fun center(v: Supplier) = apply { center = v } + + /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ + fun axis(v: Vec3) = apply { axis = v } + + /** + * 目标轨道半径(世界单位) + * + * - 值越小:轨道越紧凑 + * - 值越大:粒子绕行范围越大 + */ + fun radius(v: Double) = apply { radius = v } + + /** + * 角速度强度(每 tick 施加的切向速度增量) + * + * - 较小:缓慢绕行 + * - 较大:快速旋转(建议配合限速或阻尼) + */ + fun angularSpeed(v: Double) = apply { angularSpeed = v } + + /** + * 径向纠正强度(用于将粒子拉回目标半径) + * + * - 值越小:允许轨道半径有较大漂移 + * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 + */ + fun radialCorrect(v: Double) = apply { radialCorrect = v } + + /** + * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) + */ + fun minDistance(v: Double) = apply { minDistance = v } + + /** + * 轨道模式 + * + * - PHYSICAL:真实物理(默认) + * - SPRING:弹簧收敛(推荐通用) + * - SNAP:强制吸附(最稳定) + */ + fun mode(v: OrbitMode) = apply { mode = v } + + /** + * 仅在 PHYSICAL / SPRING 下生效: + * 每 tick 最大径向修正量(防止启动时被拉飞) + */ + fun maxRadialStep(v: Double) = apply { maxRadialStep = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val ax = axis.normalize() + val centerPos = center.get() + + val r0 = pos.subtract(centerPos) + val axialComp = ax.scale(r0.dot(ax)) + var radial = r0.subtract(axialComp) + + var dist = radial.length() + dist = max(dist, minDistance) + + radial = radial.scale(1.0 / dist) + + // 切向方向 + var tangential = ax.cross(radial) + val tLen = tangential.length() + if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) + + val dvTan = tangential.scale(angularSpeed) + val err = dist - radius + + when (mode) { + OrbitMode.PHYSICAL -> { + val raw = -err * radialCorrect + val step = raw.coerceIn(-maxRadialStep, maxRadialStep) + val dvRad = radial.scale(step) + data.velocity = data.velocity.add(dvTan).add(dvRad) + } + + OrbitMode.SPRING -> { + // 二阶感:误差越大,拉力越强,但更温和 + val spring = -err * radialCorrect + val step = spring.coerceIn(-maxRadialStep, maxRadialStep) + val dvRad = radial.scale(step) + data.velocity = data.velocity.add(dvTan).add(dvRad) + } + + OrbitMode.SNAP -> { + // 直接把粒子“吸附”到目标半径 + val targetPos = centerPos + .add(axialComp) + .add(radial.scale(radius)) + + val snapVec = targetPos.subtract(pos) + data.velocity = data.velocity.add(dvTan).add(snapVec.scale(radialCorrect)) + } + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt new file mode 100644 index 00000000..29e483e5 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt @@ -0,0 +1,98 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier + +/** + * 旋转力:让粒子绕某个轴产生切向速度(像被“搅拌”)。 + * + * 典型效果: + * - 火焰上升的旋涡感 + * - 魔法阵环绕 + * - 爆炸烟雾“打旋” + */ +class ParticleRotationForceCommand() : ParticleCommand { + + /** 旋转中心(世界坐标)。 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** + * 旋转轴(会被自动 normalize)。 + * + * - (0,1,0) 绕 Y 轴旋转(最常用) + * - (1,0,0) 绕 X + * - (0,0,1) 绕 Z + */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 旋转强度(每 tick 切向速度增量)。 + * + * - 0.05 ~ 0.3:轻微旋 + * - 0.3 ~ 2.0:明显旋 + * - >2.0:非常暴力(建议配合阻尼/限速) + */ + var strength: Double = 0.35 + + /** + * 距离衰减范围(>0)。 + * + * - 越大:远处仍然能被带动旋转 + * - 越小:只在中心附近打旋 + */ + var range: Double = 8.0 + + /** 衰减幂次(>=1)。 */ + var falloffPower: Double = 2.0 + + /** + * @param center 旋转中心 + * @param axis 旋转轴(自动 normalize) + * @param strength 旋转强度(每 tick 切向速度增量) + * @param range 衰减范围尺度 + * @param falloffPower 衰减幂次(>=1) + */ + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + strength: Double = 0.35, + range: Double = 8.0, + falloffPower: Double = 2.0, + ) : this() { + this.center = center + this.axis = axis + this.strength = strength + this.range = range + this.falloffPower = falloffPower + } + + fun center(v: Supplier) = apply { + center = v + } + fun axis(v: Vec3) = apply { axis = v } + fun strength(v: Double) = apply { strength = v } + fun range(v: Double) = apply { range = v } + fun falloffPower(v: Double) = apply { falloffPower = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val r = pos.subtract(center.get()) + val dist = r.length() + if (dist < 1e-9) return + + val ax = axis.normalize() + // tangential direction: axis x r + var t = ax.cross(r) + val tLen = t.length() + if (tLen < 1e-9) return + t = t.scale(1.0 / tLen) + + val falloff = GraphMathHelper.inversePowerFalloff(dist, range, falloffPower) + val dv = t.scale(strength * falloff) + + data.velocity = data.velocity.add(dv) + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt new file mode 100644 index 00000000..69a591f2 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt @@ -0,0 +1,226 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.extend.plus +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.abs +import kotlin.math.sqrt + +/** + * 环面回流场。 + * + * 在一圈局部区域里制造“翻卷”轨迹,适合蘑菇云帽檐、烟团边缘这类效果。 + * 它会优先“扭转当前速度方向”,而不是把粒子硬拽成一个环, + * 所以在有阻尼/减速时,卷动也会跟着自然变慢。 + * 它改的是粒子速度,不是 billboard 朝向。 + */ +class ParticleToroidalCirculationCommand : ParticleCommand { + /** 环流中心。做蘑菇云时一般填帽子中心。 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** 主轴,默认 Y 轴。 */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** 主半径,也就是翻卷带离中心的距离。 */ + var ringRadius: Double = 3.0 + + /** 径向厚度。 */ + var radialThickness: Double = 1.2 + + /** 轴向厚度。 */ + var axialThickness: Double = 0.8 + + /** 翻卷力度,负数表示反向。 */ + var circulationStrength: Double = 0.35 + + /** 向外撑开的附加力度。 */ + var outwardStrength: Double = 0.0 + + /** 向上抬的附加力度。 */ + var upwardStrength: Double = 0.0 + + /** 往翻卷带回带/导向的力度。主要用于带外把速度重新导回卷动区。 */ + var followStrength: Double = 0.12 + + /** 单 tick 最大修正量。<=0 不限制。 */ + var maxStep: Double = 0.6 + + /** 是否按生命周期减弱。 */ + var useLifeCurve: Boolean = false + + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + ringRadius: Double = 3.0, + radialThickness: Double = 1.2, + axialThickness: Double = 0.8, + circulationStrength: Double = 0.35, + outwardStrength: Double = 0.0, + upwardStrength: Double = 0.0, + followStrength: Double = 0.12, + maxStep: Double = 0.6, + useLifeCurve: Boolean = false, + ) { + this.center = center + this.axis = axis + this.ringRadius = ringRadius + this.radialThickness = radialThickness + this.axialThickness = axialThickness + this.circulationStrength = circulationStrength + this.outwardStrength = outwardStrength + this.upwardStrength = upwardStrength + this.followStrength = followStrength + this.maxStep = maxStep + this.useLifeCurve = useLifeCurve + } + + fun center(v: Supplier) = apply { center = v } + + fun center(v: Vec3) = apply { center = Supplier { v } } + + fun center(x: Double, y: Double, z: Double) = apply { center = Supplier { Vec3(x, y, z) } } + + fun axis(v: Vec3) = apply { axis = v } + + fun axis(x: Double, y: Double, z: Double) = apply { axis = Vec3(x, y, z) } + + fun ringRadius(v: Double) = apply { ringRadius = v } + + fun radialThickness(v: Double) = apply { radialThickness = v } + + fun axialThickness(v: Double) = apply { axialThickness = v } + + /** 同时设置两个厚度,方便网页只留一个输入框。 */ + fun thickness(v: Double) = apply { + radialThickness = v + axialThickness = v + } + + fun circulationStrength(v: Double) = apply { circulationStrength = v } + + + fun outwardStrength(v: Double) = apply { outwardStrength = v } + + fun upwardStrength(v: Double) = apply { upwardStrength = v } + + fun followStrength(v: Double) = apply { followStrength = v } + + fun maxStep(v: Double) = apply { maxStep = v } + + fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val centerPos = center.get() + val ax = safeNormalize(axis) + val pos = particle.loc + val rel = pos.subtract(centerPos) + + val axialDistance = rel.dot(ax) + val planar = rel.subtract(ax.scale(axialDistance)) + val planarLen = planar.length() + + val radialDir = if (planarLen < 1e-9) { + anyPerp(ax) + } else { + planar.scale(1.0 / planarLen) + } + + val qr = planarLen - ringRadius + + val radialSize = radialThickness.coerceAtLeast(1e-6) + val axialSize = axialThickness.coerceAtLeast(1e-6) + + val localRadial = qr / radialSize + val localAxial = axialDistance / axialSize + val normalizedDistance = sqrt(localRadial * localRadial + localAxial * localAxial) + + val insideBand = normalizedDistance < 1.0 + val bandWeight = if (insideBand) smooth01(1.0 - normalizedDistance) else 0.0 + val outerDistance = (normalizedDistance - 1.0).coerceAtLeast(0.0) + val captureWeight = if (outerDistance > 0.0) { + 1.0 / (1.0 + outerDistance * outerDistance * 4.0) + } else { + 0.0 + } + val lifeMul = if (useLifeCurve && particle.lifetime > 0) { + (1.0 - particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) + } else { + 1.0 + } + if (lifeMul <= 1e-9) { + return + } + + val currentVelocity = if (data.velocity.lengthSqr() > 1e-12) data.velocity else particle.velocity + val currentSpeed = currentVelocity.length() + if (currentSpeed <= 1e-9) { + return + } + val currentDir = currentVelocity.scale(1.0 / currentSpeed) + + val circulationVector = radialDir.scale(-localAxial) + .add(ax.scale(localRadial)) + val toBandCenter = radialDir.scale(-localRadial).add(ax.scale(-localAxial)) + + var desiredFlow = Vec3.ZERO + + val circulationWeight = bandWeight * lifeMul + if (circulationStrength != 0.0 && circulationWeight > 1e-9 && circulationVector.lengthSqr() > 1e-12) { + desiredFlow = desiredFlow.add(circulationVector.scale(circulationStrength * circulationWeight)) + } + if (insideBand && outwardStrength != 0.0 && circulationWeight > 1e-9) { + desiredFlow = desiredFlow.add(radialDir.scale(outwardStrength * circulationWeight)) + } + if (insideBand && upwardStrength != 0.0 && circulationWeight > 1e-9) { + desiredFlow = desiredFlow.add(ax.scale(upwardStrength * circulationWeight)) + } + + val captureMul = captureWeight * lifeMul + if (followStrength != 0.0 && captureMul > 1e-9 && toBandCenter.lengthSqr() > 1e-12) { + desiredFlow = desiredFlow.add(normalizeOrZero(toBandCenter).scale(followStrength * captureMul)) + } + + val desiredFlowLen = desiredFlow.length() + if (desiredFlowLen <= 1e-9) { + return + } + val desiredDir = desiredFlow.scale(1.0 / desiredFlowLen) + val turnedDir = normalizeOrZero(currentDir.add(desiredDir.scale(desiredFlowLen))) + if (turnedDir.lengthSqr() <= 1e-12) { + return + } + + var dv = turnedDir.scale(currentSpeed).subtract(currentVelocity) + + val dvLen = dv.length() + if (maxStep > 0.0 && dvLen > maxStep) { + dv = dv.scale(maxStep / dvLen) + } + + data.velocity += dv + } + + private fun safeNormalize(v: Vec3): Vec3 { + val len = v.length() + return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) + } + + private fun normalizeOrZero(v: Vec3): Vec3 { + val len = v.length() + return if (len < 1e-9) Vec3.ZERO else v.scale(1.0 / len) + } + + private fun anyPerp(axis: Vec3): Vec3 { + val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) + val perp = axis.cross(ref) + val len = perp.length() + return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) + } + + private fun smooth01(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + return clamped * clamped * (3.0 - 2.0 * clamped) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt new file mode 100644 index 00000000..4c86af8d --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt @@ -0,0 +1,161 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.utils.GraphMathHelper +import net.minecraft.world.phys.Vec3 +import java.util.function.Supplier +import kotlin.math.max + +/** + * 漩涡:让粒子围绕轴旋转,并可向轴线“吸入”,以及沿轴“上升/下沉”。 + * + * 常用组合: + * - swirlStrength > 0 : 打旋 + * - radialPull > 0 : 往轴线收拢(像龙卷风) + * - axialLift > 0 : 沿轴向上(像火焰/风柱) + */ +class ParticleVortexCommand() : ParticleCommand { + + /** 漩涡中心点(轴线穿过此点)。 */ + var center: Supplier = Supplier { Vec3.ZERO } + + /** 轴方向(自动 normalize)。 */ + var axis: Vec3 = Vec3(0.0, 1.0, 0.0) + + /** + * 切向旋转强度(每 tick 切向速度增量) + * + * - 0.1 ~ 0.8:柔和漩涡 + * - 0.8 ~ 3.0:明显龙卷感 + */ + var swirlStrength: Double = 0.8 + + /** + * 向轴线吸入的强度(每 tick 径向速度增量) + * + * - 0:不收拢(只打旋) + * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” + */ + var radialPull: Double = 0.35 + + /** + * 沿轴方向的升力(每 tick 轴向速度增量) + * + * - >0:沿轴正方向上升 + * - <0:沿轴反方向下沉 + */ + var axialLift: Double = 0.0 + + /** 衰减范围尺度(>0)。 */ + var range: Double = 10.0 + + /** 衰减幂次(>=1)。 */ + var falloffPower: Double = 2.0 + + /** 避免 r=0 的最小距离钳制 */ + var minDistance: Double = 0.2 + + /** + * @param center 漩涡中心(轴线穿过点) + * @param axis 轴方向(自动 normalize) + * @param swirlStrength 旋转强度(切向) + * @param radialPull 径向吸入强度 + * @param axialLift 轴向升力 + * @param range 衰减范围尺度 + * @param falloffPower 衰减幂次 + * @param minDistance 最小距离钳制,避免抖动/爆炸 + */ + constructor( + center: Supplier = Supplier { Vec3.ZERO }, + axis: Vec3 = Vec3(0.0, 1.0, 0.0), + swirlStrength: Double = 0.8, + radialPull: Double = 0.35, + axialLift: Double = 0.0, + range: Double = 10.0, + falloffPower: Double = 2.0, + minDistance: Double = 0.2, + ) : this() { + this.center = center + this.axis = axis + this.swirlStrength = swirlStrength + this.radialPull = radialPull + this.axialLift = axialLift + this.range = range + this.falloffPower = falloffPower + this.minDistance = minDistance + } + + /** 漩涡中心点(轴线穿过此点)。 */ + fun center(v: Supplier) = apply { center = v } + + /** 轴方向(自动 normalize)。 */ + fun axis(v: Vec3) = apply { axis = v } + + /** + * 切向旋转强度(每 tick 切向速度增量) + * + * - 0.1 ~ 0.8:柔和漩涡 + * - 0.8 ~ 3.0:明显龙卷感 + */ + fun swirlStrength(v: Double) = apply { swirlStrength = v } + + /** + * 向轴线吸入的强度(每 tick 径向速度增量) + * + * - 0:不收拢(只打旋) + * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” + */ + fun radialPull(v: Double) = apply { radialPull = v } + + /** + * 沿轴方向的升力(每 tick 轴向速度增量) + * + * - >0:沿轴正方向上升 + * - <0:沿轴反方向下沉 + */ + fun axialLift(v: Double) = apply { axialLift = v } + + /** 衰减范围尺度(>0)。 */ + fun range(v: Double) = apply { range = v } + + /** 衰减幂次(>=1)。 */ + fun falloffPower(v: Double) = apply { falloffPower = v } + + /** 避免 r=0 的最小距离钳制 */ + fun minDistance(v: Double) = apply { minDistance = v } + + override fun execute(data: ControlableParticleData, particle: ControlableParticle) { + val pos = particle.loc + val ax = axis.normalize() + + // vector from center + val r = pos.subtract(center.get()) + + // remove axial component -> radial vector to axis line + val axialComp = ax.scale(r.dot(ax)) + val radial = r.subtract(axialComp) + + var dist = radial.length() + if (dist < 1e-9) dist = 0.0 + + val d = max(dist, minDistance) + val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) + + // tangential direction + var tangential = ax.cross(radial) + val tLen = tangential.length() + if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) + + // radial inward direction + var inward = Vec3.ZERO + val rLen = radial.length() + if (rLen > 1e-9) inward = radial.scale(-1.0 / rLen) + + val dv = tangential.scale(swirlStrength * falloff) + .add(inward.scale(radialPull * falloff)) + .add(ax.scale(axialLift * falloff)) + + data.velocity = data.velocity.add(dv) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt new file mode 100644 index 00000000..618855d8 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt @@ -0,0 +1,10 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +data class BezierFloatKeyframe( + val time: Double, + val value: Double, + val outX: Double = 0.0, + val outY: Double = 0.0, + val inX: Double = 0.0, + val inY: Double = 0.0 +) diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt new file mode 100644 index 00000000..b8838ff9 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt @@ -0,0 +1,250 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +import kotlin.math.abs +import kotlin.math.sqrt + +class BezierKeyframeFloatCurve( + frames: List = listOf(BezierFloatKeyframe(0.0, 0.0)) +) : FloatCurve { + private val keyframes: MutableList = + frames.map { it.withClampedTime() } + .sortedBy { it.time } + .toMutableList() + + init { + if (keyframes.isEmpty()) { + keyframes.add(BezierFloatKeyframe(0.0, 0.0)) + } + } + + fun setFrames(frames: List) = apply { + keyframes.clear() + keyframes.addAll( + frames.map { it.withClampedTime() } + .sortedBy { it.time } + ) + if (keyframes.isEmpty()) { + keyframes.add(BezierFloatKeyframe(0.0, 0.0)) + } + } + + fun addFrame( + time: Double, + value: Double, + outX: Double = 0.0, + outY: Double = 0.0, + inX: Double = 0.0, + inY: Double = 0.0 + ) = apply { + keyframes.add( + BezierFloatKeyframe( + time = time.coerceIn(0.0, 1.0), + value = value, + outX = outX, + outY = outY, + inX = inX, + inY = inY + ) + ) + keyframes.sortBy { it.time } + } + + /** + * 返回当前锚点和控制柄的不可变快照。 + * + * 示例:`CParticleCurve.fromFloatCurve(curve)` 使用此快照保留 GPU 控制柄。 + * 禁止:修改返回列表不会改变此曲线;需要修改时应调用 [setFrames]。 + * + * @return 按时间升序排列的当前关键帧 + */ + fun frames(): List = keyframes.toList() + + override fun sample(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + if (keyframes.size == 1) { + return keyframes[0].value + } + if (clamped <= keyframes.first().time) { + return keyframes.first().value + } + if (clamped >= keyframes.last().time) { + return keyframes.last().value + } + + for (i in 1 until keyframes.size) { + val prev = keyframes[i - 1] + val next = keyframes[i] + if (clamped <= next.time) { + return sampleSegment(prev, next, clamped) + } + } + return keyframes.last().value + } + + private fun sampleSegment(prev: BezierFloatKeyframe, next: BezierFloatKeyframe, time: Double): Double { + val span = next.time - prev.time + if (span <= EPSILON) { + return next.value + } + + val p0x = prev.time + val p1x = prev.time + prev.outX / 100.0 + val p2x = next.time + next.inX / 100.0 + val p3x = next.time + + val p0y = prev.value + val p1y = prev.value + prev.outY + val p2y = next.value + next.inY + val p3y = next.value + + val linearGuess = ((time - prev.time) / span).coerceIn(0.0, 1.0) + val u = solveParameterForX(time, p0x, p1x, p2x, p3x, linearGuess) + return cubicBezier(u, p0y, p1y, p2y, p3y) + } + + private fun solveParameterForX( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double, + guess: Double + ): Double { + if (p0 <= p1 && p1 <= p2 && p2 <= p3) { + return solveMonotonicParameter(targetX, p0, p1, p2, p3) + } + + val roots = mutableListOf() + val splitPoints = derivativeRoots(p0, p1, p2, p3) + .filter { it > EPSILON && it < 1.0 - EPSILON } + .plus(0.0) + .plus(1.0) + .sorted() + + for (i in 1 until splitPoints.size) { + val lo = splitPoints[i - 1] + val hi = splitPoints[i] + val flo = cubicBezier(lo, p0, p1, p2, p3) - targetX + val fhi = cubicBezier(hi, p0, p1, p2, p3) - targetX + when { + abs(flo) <= EPSILON -> roots.add(lo) + abs(fhi) <= EPSILON -> roots.add(hi) + flo < 0.0 && fhi > 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) + flo > 0.0 && fhi < 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) + } + } + + return roots.distinctBy { (it / EPSILON).toLong() } + .minByOrNull { abs(it - guess) } + ?: closestSampledParameter(targetX, p0, p1, p2, p3, guess) + } + + private fun solveMonotonicParameter( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double + ): Double { + var lo = 0.0 + var hi = 1.0 + var mid = 0.5 + repeat(32) { + mid = (lo + hi) * 0.5 + if (cubicBezier(mid, p0, p1, p2, p3) < targetX) { + lo = mid + } else { + hi = mid + } + } + return mid + } + + private fun solveBracketedParameter( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double, + startLo: Double, + startHi: Double + ): Double { + var lo = startLo + var hi = startHi + var mid = (lo + hi) * 0.5 + val increasing = cubicBezier(hi, p0, p1, p2, p3) >= cubicBezier(lo, p0, p1, p2, p3) + repeat(32) { + mid = (lo + hi) * 0.5 + val x = cubicBezier(mid, p0, p1, p2, p3) + if ((x < targetX) == increasing) { + lo = mid + } else { + hi = mid + } + } + return mid + } + + private fun derivativeRoots(p0: Double, p1: Double, p2: Double, p3: Double): List { + val a = -p0 + 3.0 * p1 - 3.0 * p2 + p3 + val b = 3.0 * p0 - 6.0 * p1 + 3.0 * p2 + val c = -3.0 * p0 + 3.0 * p1 + + val qa = 3.0 * a + val qb = 2.0 * b + val qc = c + if (abs(qa) <= EPSILON) { + return if (abs(qb) <= EPSILON) emptyList() else listOf(-qc / qb) + } + + val discriminant = qb * qb - 4.0 * qa * qc + if (discriminant < 0.0) { + return emptyList() + } + + val root = sqrt(discriminant) + return listOf( + (-qb - root) / (2.0 * qa), + (-qb + root) / (2.0 * qa) + ) + } + + private fun closestSampledParameter( + targetX: Double, + p0: Double, + p1: Double, + p2: Double, + p3: Double, + guess: Double + ): Double { + var best = guess + var bestError = abs(cubicBezier(guess, p0, p1, p2, p3) - targetX) + for (i in 0..64) { + val u = i / 64.0 + val error = abs(cubicBezier(u, p0, p1, p2, p3) - targetX) + if (error < bestError) { + best = u + bestError = error + } + } + return best + } + + private fun cubicBezier(t: Double, p0: Double, p1: Double, p2: Double, p3: Double): Double { + val u = 1.0 - t + val u2 = u * u + val t2 = t * t + return u2 * u * p0 + + 3.0 * u2 * t * p1 + + 3.0 * u * t2 * p2 + + t2 * t * p3 + } + + private fun BezierFloatKeyframe.withClampedTime(): BezierFloatKeyframe { + return copy(time = time.coerceIn(0.0, 1.0)) + } + + private companion object { + const val EPSILON = 1.0E-9 + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt new file mode 100644 index 00000000..2717be1f --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt @@ -0,0 +1,18 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +/** + * 常量曲线:全生命周期输出同一个值。 + * + * 典型用途: + * - 固定重力倍率 + * - 固定速度修正 + * - 作为其它更复杂曲线的默认占位 + */ +class ConstantFloatCurve( + /** + * 常量值。 + */ + var value: Double = 0.0 +) : FloatCurve { + override fun sample(t: Double): Double = value +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt new file mode 100644 index 00000000..2998ffa8 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt @@ -0,0 +1,22 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +/** + * 生命周期曲线接口(标量)。 + * + * 约定: + * - 输入 `t` 是生命周期进度,通常期望在 `[0,1]` 范围内 + * - 实现类应尽量在内部做 `t` 的钳制,避免调用方传入异常值时不稳定 + * + * 用途: + * - 给粒子命令(如 Force/Velocity/Inherit)提供“随时间变化”的系数 + * - 统一不同曲线实现(常量、线性、关键帧)的采样入口 + */ +fun interface FloatCurve { + /** + * 采样曲线值。 + * + * @param t 生命周期进度(通常是 `currentAge / lifetime`) + * @return 曲线在该进度下的值 + */ + fun sample(t: Double): Double +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt new file mode 100644 index 00000000..d8953cf5 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt @@ -0,0 +1,12 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +/** + * 标量关键帧。 + * + * @property time 关键帧时间点,建议使用 `[0,1]` 的生命周期归一化时间 + * @property value 关键帧数值 + */ +data class FloatKeyframe( + val time: Double, + val value: Double +) diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt new file mode 100644 index 00000000..56253661 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt @@ -0,0 +1,78 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +/** + * 关键帧曲线(线性插值)。 + * + * 规则: + * - 帧会按 `time` 升序保存 + * - `t` 落在相邻关键帧之间时,使用线性插值 + * - `t` 小于首帧或大于尾帧时,返回边界帧值 + */ +class KeyframeFloatCurve( + frames: List = listOf(FloatKeyframe(0.0, 0.0)) +) : FloatCurve { + private val keyframes: MutableList = + frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } + .sortedBy { it.time } + .toMutableList() + + init { + if (keyframes.isEmpty()) { + keyframes.add(FloatKeyframe(0.0, 0.0)) + } + } + + /** + * 批量替换关键帧列表。 + * + * - 时间会被钳制到 `[0,1]` + * - 内部会自动按时间升序排序 + */ + fun setFrames(frames: List) = apply { + keyframes.clear() + keyframes.addAll( + frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } + .sortedBy { it.time } + ) + if (keyframes.isEmpty()) { + keyframes.add(FloatKeyframe(0.0, 0.0)) + } + } + + /** + * 追加一个关键帧。 + * + * @param time 帧时间(会钳制到 `[0,1]`) + * @param value 帧值 + */ + fun addFrame(time: Double, value: Double) = apply { + keyframes.add(FloatKeyframe(time.coerceIn(0.0, 1.0), value)) + keyframes.sortBy { it.time } + } + + override fun sample(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + if (keyframes.size == 1) { + return keyframes[0].value + } + if (clamped <= keyframes.first().time) { + return keyframes.first().value + } + if (clamped >= keyframes.last().time) { + return keyframes.last().value + } + + for (i in 1 until keyframes.size) { + val prev = keyframes[i - 1] + val next = keyframes[i] + if (clamped <= next.time) { + val seg = (next.time - prev.time).coerceAtLeast(1e-9) + val alpha = (clamped - prev.time) / seg + return GraphMathHelper.lerp(alpha, prev.value, next.value) + } + } + return keyframes.last().value + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt new file mode 100644 index 00000000..e11c75e5 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt @@ -0,0 +1,27 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve + +import cn.coostack.cooparticlesapi.utils.GraphMathHelper + +/** + * 线性曲线:在 `[0,1]` 区间内从 `from` 匀速过渡到 `to`。 + * + * 典型用途: + * - 生命周期内“线性增强/衰减” + * - 先快速验证一个效果趋势,再换成关键帧曲线精调 + */ +class LinearFloatCurve( + /** + * `t=0` 时的值。 + */ + var from: Double = 0.0, + + /** + * `t=1` 时的值。 + */ + var to: Double = 1.0 +) : FloatCurve { + override fun sample(t: Double): Double { + val clamped = t.coerceIn(0.0, 1.0) + return GraphMathHelper.lerp(clamped, from, to) + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt new file mode 100644 index 00000000..f3077345 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt @@ -0,0 +1,77 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import com.ezylang.evalex.Expression +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 + +class BallWindDirection( + override var direction: Vec3, + var radius: Double, + var offset: RelativeLocation, +) : WindDirection { + override var relative: Boolean = false + override var windSpeedExpress: String = "1" + override fun loadEmitters(emitters: ParticleEmitters): WindDirection { + this.emitters = emitters + return this + } + + override fun hasLoadedEmitters(): Boolean { + return emitters != null + } + + private var emitters: ParticleEmitters? = null + + companion object { + @JvmStatic + val CODEC = StreamCodec.of( + { buf, data -> + data as BallWindDirection + buf.writeVec3(data.direction) + buf.writeBoolean(data.relative) + buf.writeUtf(data.windSpeedExpress) + buf.writeVec3(data.offset.toVector()) + buf.writeDouble(data.radius) + }, { + val direction = it.readVec3() + val relative = it.readBoolean() + val express = it.readUtf() + val offset = RelativeLocation.of(it.readVec3()) + val radius = it.readDouble() + BallWindDirection(direction, radius, offset).apply { + this.relative = relative + this.windSpeedExpress = express + } + } + ) + const val ID = "ball" + } + + override fun getID(): String { + return ID + } + + override fun getWind(particlePos: Vec3): Vec3 { + if (relative) { + val pos = emitters?.pos ?: return direction + val dir = particlePos.subtract(pos) + val express = Expression(windSpeedExpress) + .with("l", dir.length()) + .evaluate().numberValue.toDouble() + dir.normalize().scale(express) + return dir + } + return direction + } + + override fun inRange(pos: Vec3): Boolean { + return emitters!!.pos.add(offset.toVector()).subtract(pos).length() <= radius + } + + override fun getCodec(): StreamCodec { + return CODEC + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt new file mode 100644 index 00000000..18703ca0 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt @@ -0,0 +1,92 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.barrages.HitBox +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import com.ezylang.evalex.Expression +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 +import kotlin.math.exp + +class BoxWindDirection( + override var direction: Vec3, + var box: HitBox, + var offset: RelativeLocation, +) : WindDirection { + override var relative: Boolean = false + override var windSpeedExpress: String = "1" + override fun loadEmitters(emitters: ParticleEmitters): WindDirection { + this.emitters = emitters + return this + } + + private var emitters: ParticleEmitters? = null + + companion object { + @JvmStatic + val CODEC = StreamCodec.of( + { buf, data -> + data as BoxWindDirection + buf.writeVec3(data.direction) + buf.writeBoolean(data.relative) + buf.writeUtf(data.windSpeedExpress) + buf.writeVec3(data.offset.toVector()) + buf.writeDouble(data.box.x1) + buf.writeDouble(data.box.x2) + buf.writeDouble(data.box.y1) + buf.writeDouble(data.box.y2) + buf.writeDouble(data.box.z1) + buf.writeDouble(data.box.z2) + }, { + val direction = it.readVec3() + val relative = it.readBoolean() + val express = it.readUtf() + val offset = RelativeLocation.of(it.readVec3()) + val box = HitBox( + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + ) + BoxWindDirection(direction, box, offset).apply { + this.relative = relative + this.windSpeedExpress = express + } + } + ) + const val ID = "box" + } + + override fun getID(): String { + return ID + } + + override fun hasLoadedEmitters(): Boolean { + return emitters != null + } + + override fun getWind(particlePos: Vec3): Vec3 { + if (relative) { + val pos = emitters?.pos ?: return direction + val dir = particlePos.subtract(pos) + val express = Expression(windSpeedExpress) + .with("l", dir.length()) + .evaluate().numberValue.toDouble() + dir.normalize().scale(express) + return dir + } + return direction + } + + override fun inRange(pos: Vec3): Boolean { + val ofBox = box.ofBox(emitters!!.pos.add(offset.toVector())) + return ofBox.contains(pos) + } + + override fun getCodec(): StreamCodec { + return CODEC + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt new file mode 100644 index 00000000..2f97c8ae --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt @@ -0,0 +1,68 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import com.ezylang.evalex.Expression +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 +import kotlin.math.exp + +class GlobalWindDirection( + override var direction: Vec3 +) : WindDirection { + override var relative: Boolean = false + override var windSpeedExpress: String = "1" + override fun loadEmitters(emitters: ParticleEmitters): WindDirection { + this.emitters = emitters + return this + } + + private var emitters: ParticleEmitters? = null + + companion object { + @JvmStatic + val CODEC = StreamCodec.of( + { buf, data -> + buf.writeVec3(data.direction) + buf.writeBoolean(data.relative) + buf.writeUtf(data.windSpeedExpress) + }, { + val direction = it.readVec3() + val relative = it.readBoolean() + val express = it.readUtf() + GlobalWindDirection(direction).apply { + this.relative = relative + this.windSpeedExpress = express + } + } + ) + const val ID = "global" + } + + override fun getID(): String { + return ID + } + override fun hasLoadedEmitters(): Boolean { + return emitters != null + } + override fun getWind(particlePos: Vec3): Vec3 { + if (relative) { + val pos = emitters?.pos ?: return direction + val dir = particlePos.subtract(pos) + val express = Expression(windSpeedExpress) + .with("l", dir.length()) + .evaluate().numberValue.toDouble() + dir.normalize().scale(express) + return dir + } + return direction + } + + override fun inRange(pos: Vec3): Boolean { + return true + } + + override fun getCodec(): StreamCodec { + return CODEC + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt new file mode 100644 index 00000000..d080bd33 --- /dev/null +++ b/neoforge/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 cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 + +interface WindDirection { + /** + * 风向 + * 如果relative 为true 则此选项作废 + * direction.length() 代表风速大小 + * 如果设置为0向量 则代表没有风 + * relative为true 时代表粒子相对发射器位置为风向 + */ + var direction: Vec3 + var relative: Boolean + + /** + * relative 为false时此功能不启用 + * 若relative为true + * 则此参数代表风力大小 + * 表达式采用EvalEx3.5.0 + * 提供的变量 l (代表粒子与发射器的实际距离) + * 返回值是风速大小(direction.length()) + */ + var windSpeedExpress: String + + fun loadEmitters(emitters: ParticleEmitters): WindDirection + + fun hasLoadedEmitters(): Boolean + + fun getID(): String + + /** + * 获得当前位置的风速向量 + */ + fun getWind(particlePos: Vec3): Vec3 + + /** + * 粒子位置是否在可操作范围内 + */ + fun inRange(pos: Vec3): Boolean + + fun getCodec(): StreamCodec +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt new file mode 100644 index 00000000..59c65a77 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt @@ -0,0 +1,57 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind + +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 + +object WindDirections { + private val packets = HashMap>() + + val GLOBAL = register("global", GlobalWindDirection.CODEC) + + val BOX = register("box", BoxWindDirection.CODEC) + + val BALL = register("ball", BallWindDirection.CODEC) + + /** + * 物理处理风力 + * @param v 先前移动方向 + * @param pos 在风场中的位置 (在粒子发射器中就是粒子位置) 如果超出范围则会返回0向量 + */ + fun handleWindForce( + wind: WindDirection, + pos: Vec3, + airDensity: Double, + dragCoefficient: Double, + crossSectionalArea: Double, + v: Vec3 = Vec3.ZERO, + ): Vec3 { + if (!wind.inRange(pos)) { + return Vec3.ZERO + } + val windVec = wind.getWind(pos) + return if (windVec.lengthSqr() > 0) { + val relativeWind = windVec.subtract(v) + val windMagnitude = 0.5 * airDensity * dragCoefficient * + crossSectionalArea * relativeWind.lengthSqr() * 0.05 + relativeWind.normalize().scale(windMagnitude) + } else { + Vec3.ZERO + } + } + + fun getCodecFromID(id: String): StreamCodec { + return packets[id]!! + } + + fun register( + id: String, + codec: StreamCodec + ): StreamCodec { + packets[id] = codec + return codec + } + + fun init() {} +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt new file mode 100644 index 00000000..0ed4fbff --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt @@ -0,0 +1,25 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.phys.BlockHitResult + +/** + * 粒子碰撞方块事件 + * 用于复用物理粒子 + */ +class ParticleCollideEvent( + override var particle: ControlableParticle, + override var particleData: ControlableParticleData, + var res: BlockHitResult +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleColliderEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt new file mode 100644 index 00000000..9d7a3cad --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters +import cn.coostack.cooparticlesapi.particles.ControlableParticle + +interface ParticleEvent { + /** + * 触发事件的粒子 + */ + var particle: ControlableParticle + var particleData: ControlableParticleData + + /** + * 取消接下来的粒子事件队列 + */ + var canceled: Boolean + + /** + * 用于方便存储 + * 获取事件ID + */ + fun getEventID(): String +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt new file mode 100644 index 00000000..e6a1b67b --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt @@ -0,0 +1,26 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +/** + * 自动注册 + * 事件处理器是单例模式 + */ +interface ParticleEventHandler : Comparable { + + fun handle(event: ParticleEvent) + + /** + * 详细见 + * 目标事件的EventID + * @see ParticleEvent.getEventID + */ + fun getTargetEventID(): String + + fun getHandlerID(): String + + fun getPriority(): Int + + override fun compareTo(other: ParticleEventHandler): Int { + return getPriority() - other.getPriority() + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt new file mode 100644 index 00000000..7a2a4446 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt @@ -0,0 +1,56 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.annotations.CooAutoRegister +import cn.coostack.cooparticlesapi.reflect.CooAPIScanner +import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo +import cn.coostack.cooparticlesapi.utils.ReflectUtil +import java.lang.reflect.Modifier + +object ParticleEventHandlerManager { + private val registerHandlers = HashMap() + + fun getHandlerById(id: String): ParticleEventHandler? { + return registerHandlers[id] + } + + fun register(event: ParticleEventHandler) { + registerHandlers[event.getHandlerID()] = event + } + + fun hasRegister(id: String): Boolean = getHandlerById(id) != null + + private var handled = false + fun registerScanner() { + if (handled) { + return + } + val before = registerHandlers.size + CooParticlesConstants.logger.info("正在自动注册 ParticleEventHandler") + var count = 0 + val start = System.currentTimeMillis() + CooAPIScanner.getWithAnnotation( + CooAutoRegister::class.java + ).forEach { + count++ + findListenerHandlers(it) + } + val end = System.currentTimeMillis() + CooParticlesConstants.logger.info("EmittersEvents 注册完成 耗时 ${end - start} ms 扫描了 $count 个类 实际注册 :${registerHandlers.size - before}") + } + + private fun findListenerHandlers(target: SimpleClassInfo) { + val clazz = target.toClass() + if (!ParticleEvent::class.java.isAssignableFrom(clazz)) { + return + } + // 获取instance + val instance = + clazz.declaredFields.find { it.name == "INSTANCE" && Modifier.isStatic(it.modifiers) }?.get(null) + ?: clazz.getDeclaredConstructor() + .apply { isAccessible = true } + .newInstance() + register(instance as ParticleEventHandler) + } + +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt new file mode 100644 index 00000000..a1a7f892 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt @@ -0,0 +1,23 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.world.entity.Entity + +/** + * 粒子接触实体事件 + */ + +class ParticleHitEntityEvent( + override var particle: ControlableParticle, override var particleData: ControlableParticleData, var hit: Entity, +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleHitEntityEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt new file mode 100644 index 00000000..3175ae3c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.Vec3 + +/** + * 粒子接触地面事件 + */ +class ParticleOnGroundEvent( + override var particle: ControlableParticle, + override var particleData: ControlableParticleData, + /** + * 击中的方块 + */ + var hit: BlockPos, + /** + * 和方块的交点 + */ + var intersection: Vec3, + var res: BlockHitResult +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleOnGroundEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt new file mode 100644 index 00000000..991cc368 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt @@ -0,0 +1,24 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.event + +import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import net.minecraft.core.BlockPos + +/** + * 粒子接触实体事件 + */ +class ParticleOnLiquidEvent( + override var particle: ControlableParticle, + override var particleData: ControlableParticleData, + var hit: BlockPos +) : ParticleEvent { + override var canceled: Boolean = false + + companion object { + const val EVENT_ID = "ParticleOnLiquidEvent" + } + + override fun getEventID(): String { + return EVENT_ID + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt new file mode 100644 index 00000000..0deb18c0 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt @@ -0,0 +1,78 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import cn.coostack.cooparticlesapi.barrages.HitBox +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 +import kotlin.random.Random + +class BoxEmittersShootType(val box: HitBox) : EmittersShootType { + companion object { + @JvmStatic + val CODEC: StreamCodec = + StreamCodec.of( + { buf, type -> + type as BoxEmittersShootType + val box = type.box + buf.writeDouble(box.x1) + buf.writeDouble(box.y1) + buf.writeDouble(box.z1) + buf.writeDouble(box.x2) + buf.writeDouble(box.y2) + buf.writeDouble(box.z2) + + }, { + val box = HitBox( + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + it.readDouble(), + ) + BoxEmittersShootType(box) + } + ) + const val ID = "box" + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): StreamCodec { + return CODEC + } + + val random = Random(System.currentTimeMillis()) + override fun getPositions(origin: Vec3, tick: Int, count: Int): List { + return List(count) { + val x = random.nextDouble( + box.x1, + box.x2 + ) + val y = random.nextDouble( + box.y1, + box.y2 + ) + val z = random.nextDouble( + box.z1, + box.z2 + ) + origin.add(Vec3(x, y, z)) + } + } + + override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { + if (enter.length() < 1e-7) { + // 随机速度 + val p = Vec3( + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0) + ) + return p + } + return enter + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt new file mode 100644 index 00000000..b3c660e9 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt @@ -0,0 +1,34 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 + +interface EmittersShootType { + /** + * 获取id 用于序列化 + */ + fun getID(): String + + /** + * 用于序列化发射类型 + */ + fun getCodec(): StreamCodec + + /** + * 获取生成粒子的位置 + * @param origin 添加offset后的位置 + * @param tick 发射器当前tick + * @param count 粒子数量 + * @return 可能返回多个粒子 + */ + fun getPositions(origin: Vec3, tick: Int, count: Int): List + + /** + * 获取粒子的初始方向 + * @param enter 输入的参数方向 + * @param pos 粒子生成位置 + * @param origin 粒子发射器的位置 + */ + fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt new file mode 100644 index 00000000..3553d490 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt @@ -0,0 +1,74 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import cn.coostack.cooparticlesapi.barrages.HitBox +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 + +object EmittersShootTypes { + + private val types = HashMap>() + + fun register(id: String, codec: StreamCodec) { + types[id] = codec + } + + fun fromID(id: String) = types[id] + + @JvmStatic + fun point(): EmittersShootType = PointEmittersShootType() + + @JvmStatic + fun box(box: HitBox): EmittersShootType = BoxEmittersShootType(box) + + @JvmStatic + fun line(dir: Vec3, step: Double): EmittersShootType = LineEmittersShootType(dir, step) + + /** + * @param xe 粒子生成 x 相对于发射器的位置偏移量表达式 + * @param ye 粒子生成 y 相对于发射器的位置偏移量表达式 + * @param ze 粒子生成 z 相对于发射器的位置偏移量表达式 + * + * @param dxe 粒子生成时 初始速度向量x表达式 + * @param dye 粒子生成时 初始速度向量y表达式 + * @param dze 粒子生成时 初始速度向量z表达式 + */ + @JvmStatic + fun math( + xe: String, + ye: String, + ze: String, + dxe: String, + dye: String, + dze: String, + ): EmittersShootType = MathEmittersShootType() + .apply { + x = xe + y = ye + z = ze + dx = dxe + dy = dye + dz = dze + setup() + } + + init { + register( + PointEmittersShootType.ID, PointEmittersShootType.CODEC + ) + register( + BoxEmittersShootType.ID, + BoxEmittersShootType.CODEC + ) + register( + MathEmittersShootType.ID, + MathEmittersShootType.CODEC + ) + register( + LineEmittersShootType.ID, + LineEmittersShootType.CODEC + ) + } + + fun init() {} +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt new file mode 100644 index 00000000..b70d51ed --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt @@ -0,0 +1,53 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 + + +class LineEmittersShootType(val dir: Vec3, val step: Double) : EmittersShootType { + companion object { + @JvmStatic + val CODEC: StreamCodec = + StreamCodec.of( + { buf, type -> + type as LineEmittersShootType + buf.writeVec3(type.dir) + buf.writeDouble(type.step) + + }, { + val dir = it.readVec3() + val step = it.readDouble() + LineEmittersShootType(dir, step) + } + ) + const val ID = "line" + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): StreamCodec { + return CODEC + } + + override fun getPositions( + origin: Vec3, + tick: Int, + count: Int + ): List { + return List(count) { + origin.add(dir.normalize().scale(it * step)) + } + } + + override fun getDefaultDirection( + enter: Vec3, + tick: Int, + pos: Vec3, + origin: Vec3 + ): Vec3 { + return enter + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt new file mode 100644 index 00000000..51d426ce --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt @@ -0,0 +1,196 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import com.ezylang.evalex.Expression +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 +import java.math.BigDecimal +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.FutureTask + +/** + * 基于EvalEx 表达式的运算 + * 支持的变量 + * 对于粒子生成位置 + * t -> tick + * c -> count + * i -> 当前索引值 + * 对于粒子初始速度 + * t -> tick + * x -> posX + * y -> posY + * z -> posZ + * ox -> origin.x + * oy -> origin.y + * oz -> origin.z + */ +class MathEmittersShootType : EmittersShootType { + companion object { + @JvmStatic + val CODEC = StreamCodec.of( + { buf, type -> + type as MathEmittersShootType + buf.writeUtf(type.x) + buf.writeUtf(type.y) + buf.writeUtf(type.z) + buf.writeUtf(type.dx) + buf.writeUtf(type.dy) + buf.writeUtf(type.dz) + }, { + MathEmittersShootType().apply { + x = it.readUtf() + y = it.readUtf() + z = it.readUtf() + dx = it.readUtf() + dy = it.readUtf() + dz = it.readUtf() + setup() + } + } + ) + const val ID = "math" + } + + /** + * 位置运算表达式 + */ + var x = "0" + var y = "0" + var z = "0" + + private var xe = Expression(x) + .with("i", 0) + .and("c", 0) + .and("t", 0) + private var ye = Expression(y) + .with("i", 0) + .and("c", 0) + .and("t", 0) + private var ze = Expression(z) + .with("i", 0) + .and("c", 0) + .and("t", 0) + + /** + * 初始方向表达式 + */ + var dx = "0" + var dy = "0" + var dz = "0" + + private var dxe = Expression(dx) + .with("t", 0) + + private var dye = Expression(dy) + .with("t", 0) + + private var dze = Expression(dz) + .with("t", 0) + + fun setup() { + xe = Expression(x) + .with("i", 0) + .and("c", 0) + .and("t", 0) + ye = Expression(y) + .with("i", 0) + .and("c", 0) + .and("t", 0) + ze = Expression(z) + .with("i", 0) + .and("c", 0) + .and("t", 0) + dxe = Expression(dx) + .with("t", 0) + + dye = Expression(dy) + .with("t", 0) + + dze = Expression(dz) + .with("t", 0) + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): StreamCodec { + return CODEC + } + + override fun getPositions( + origin: Vec3, + tick: Int, + count: Int + ): List { + + return List(count) { + val taskX = CompletableFuture.supplyAsync { + xe.with("t", tick) + .with("c", count) + .with("i", it) + .evaluate().numberValue.toDouble() + } + + val taskY = CompletableFuture.supplyAsync { + ye.with("t", tick) + .with("c", count) + .with("i", it) + .evaluate().numberValue.toDouble() + } + + val taskZ = CompletableFuture.supplyAsync { + ze.with("t", tick) + .and("c", count) + .and("i", it) + .evaluate().numberValue.toDouble() + } + CompletableFuture.allOf(taskX, taskY, taskZ).join() + origin.add(Vec3(taskX.get(), taskY.get(), taskZ.get())) + } + } + + override fun getDefaultDirection( + enter: Vec3, + tick: Int, + pos: Vec3, + origin: Vec3 + ): Vec3 { + val x = CompletableFuture.supplyAsync { + dxe.with("t", tick) + .and("x", pos.x) + .and("y", pos.y) + .and("z", pos.z) + .and("ox", origin.x) + .and("oy", origin.y) + .and("oz", origin.z) + .evaluate().numberValue.toDouble() + } + val y = CompletableFuture.supplyAsync { + dye.with("t", tick) + .and("x", pos.x) + .and("y", pos.y) + .and("z", pos.z) + .and("ox", origin.x) + .and("oy", origin.y) + .and("oz", origin.z) + .evaluate().numberValue.toDouble() + } + val z = CompletableFuture.supplyAsync { + dze.with("t", tick) + .and("x", pos.x) + .and("y", pos.y) + .and("z", pos.z) + .and("ox", origin.x) + .and("oy", origin.y) + .and("oz", origin.z) + .evaluate().numberValue.toDouble() + } + CompletableFuture.allOf(x, y, z).join() + return enter.add( + x.get(), y.get(), z.get() + ) + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt new file mode 100644 index 00000000..72df0b76 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt @@ -0,0 +1,48 @@ +package cn.coostack.cooparticlesapi.network.particle.emitters.type + +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.world.phys.Vec3 +import kotlin.random.Random + +class PointEmittersShootType : EmittersShootType { + val random = Random(System.currentTimeMillis()) + + companion object { + @JvmStatic + val CODEC: StreamCodec = + StreamCodec.of( + { b, p -> }, { + PointEmittersShootType() + } + ) + const val ID = "point" + } + + override fun getID(): String { + return ID + } + + override fun getCodec(): StreamCodec { + return CODEC + } + + override fun getPositions(origin: Vec3, tick: Int, count: Int): List { + return List(count) { + origin + } + } + + override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { + if (enter.length() < 1e-7) { + // 随机速度 + val p = Vec3( + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0), + random.nextDouble(-1.0, 1.0) + ) + return p + } + return enter + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt new file mode 100644 index 00000000..f98bbc30 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt @@ -0,0 +1,573 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C +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.cparticle.CParticleSystem +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable +import cn.coostack.cooparticlesapi.cparticle.compat.CParticleDisplayer +import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition +import cn.coostack.cooparticlesapi.particles.ControlableParticle +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.particles.control.ControlParticleManager +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.particles.control.ParticleControler +import cn.coostack.cooparticlesapi.particles.control.RemoveReason +import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup +import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.set +import kotlin.math.PI + +/** 客户端渲染和服务端处理都用这个类 */ +@Deprecated("使用ParticleComposition") +abstract class ParticleGroupStyle(var visibleRange: Double = 32.0, val uuid: UUID = UUID.randomUUID()) : + Controlable, ServerControler, Tickable { + var world: Level? = null + var pos: Vec3 = Vec3.ZERO + var client = false + var rotate = 0.0 + var axis = RelativeLocation.yAxis() + var scale = 1.0 + set(value) { + field = value.coerceAtLeast(0.001) + } + + var positionDirty = false + + + /** + * 上一次更新的游戏时间 + * > 用来解决回放的时候会产生额外的粒子 或者导致缺少粒子 (额外tick) + */ + + var lastUpdatedGameTime = 0L + + /** 生成粒子样式的时间 可能会直接跳转到其他时候 然后这个时候这个参数就需要进行同步 */ + var displayedTime = 0L + + /** 自动发包同步到客户端 可能会占据大量带宽 */ + var autoToggle = false + + var displayed = false + internal set + internal var valid = true + internal val invokeQueue = ArrayList Unit>() + internal val postInvokeQueue = ArrayList Unit>() + val particles = ConcurrentHashMap>() + val particleLocations = ConcurrentHashMap, RelativeLocation>() + private val cParticleSystems = LinkedHashMap() + private val cParticleContainers = LinkedHashSet>() + + internal fun getCParticleSystems(): List { + removeReleasedCParticleSystems() + return cParticleSystems.keys.toList() + } + + internal fun getCParticleContainers(): List> = + cParticleContainers.toList() + + protected fun prepareCParticleDisplayer(displayer: ParticleDisplayer, capacity: Int) { + if (displayer is CParticleDisplayer) { + displayer.bindDedicatedSystemIfAbsent("style/$uuid", capacity, pos) + } + } + + protected fun registerCParticleNode(controler: Controlable<*>) { + removeReleasedCParticleSystems() + when (controler) { + is CParticleControlable -> cParticleSystems.merge(controler.system, 1, Int::plus) + is ParticleComposition, + is ParticleGroupStyle, + is ControlableParticleGroup -> cParticleContainers.add(controler) + } + } + + protected fun unregisterCParticleNode(controler: Controlable<*>) { + when (controler) { + is CParticleControlable -> { + val count = cParticleSystems[controler.system] ?: return + if (count <= 1) cParticleSystems.remove(controler.system) + else cParticleSystems[controler.system] = count - 1 + } + + is ParticleComposition, + is ParticleGroupStyle, + is ControlableParticleGroup -> cParticleContainers.remove(controler) + } + } + + private fun removeReleasedCParticleSystems() { + cParticleSystems.keys.removeIf { it.released } + } + + override fun isValid(): Boolean { + return valid + } + + /** 当粒子组合初始化时, 存储1倍缩放粒子组与原点的距离 */ + val particleDefaultLength = ConcurrentHashMap() + + + abstract fun getCurrentFrames(): Map + + abstract fun onDisplay() + + /** + * 服务器同步到客户端时, 执行的代码 基类已经存储的参数 如 pos, world ,rotate, axis, scale, uuid 无需同步 + * 自定义的其他参数需要同步 + * + * 如果 autoToggle = true 则会在每个tick都会执行发包 反复调用此方法 + */ + abstract fun writePacketArgs(): Map> + + /** + * 客户端接受服务器的同步时, 使用的代码 + * + * 同步内容如下 -> 客户端创建此类时 输入的基本参数 包括了你在 writePacketArgs输入的参数 客户端在类内存在其余更改时 + * 服务器传入的参数 比如自己设定的一些其他参数值 + * + * 无需处理以下参数 pos world rotate axis scale uuid 其余参数自行处理 + */ + abstract fun readPacketArgs(args: Map>) + + open fun beforeDisplay(styles: Map) { + } + + override fun addPreTickAction(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { + invokeQueue.add(action) + return this + } + + override fun addPreTickActionPost(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { + postInvokeQueue.add(action) + return this + } + + override fun controlUUID(): UUID { + return uuid + } + + override fun rotateToPoint(to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + particleLocations.values.toList(), to, axis + ) + axis = to + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to) + ) + ) + } + } + + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { +// Math3DUtil.rotateAsAxis( +// particleLocations.values.toList(), axis, angle +// ) +// Math3DUtil.rotatePointsToPoint( +// particleLocations.values.toList(), to, axis +// ) + Math3DUtil.rotateToWithRoll( + particleLocations.values.toList(), axis, to, radian + ) + axis = to + this.rotate += radian + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to), + "rotate_angle" to ParticleControlerDataBuffers.double(radian) + ) + ) + } + } + + override fun rotateAsAxis(angle: Double) { + Math3DUtil.rotateAsAxis( + particleLocations.values.toList(), axis, angle + ) + this.rotate += angle + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "rotate_angle" to ParticleControlerDataBuffers.double(angle) + ) + ) + } + } + + override fun teleportTo(pos: Vec3) { + this.pos = pos + toggleRelative() + if (!client) { + // 同步到其他客户端 + change( + mapOf( + "teleport" to ParticleControlerDataBuffers.vec3d(pos) + ) + ) + } + } + + override fun teleportTo(x: Double, y: Double, z: Double) { + teleportTo(Vec3(x, y, z)) + } + + override fun remove() { + clear(false) + if (!client) { + // 同步到其他客户端 + ParticleStyleManager.filterVisiblePlayer(this).forEach { + val player = world!!.getPlayerByUUID(it) as? ServerPlayer + player ?: return@forEach + CooParticlesServices.SERVER_NETWORK.send( + PacketParticleStyleS2C( + uuid, ControlType.REMOVE, mapOf() + ), player + ) + } + } + } + + /** Controler需求 */ + override fun getControlObject(): ParticleGroupStyle { + return this + } + + /** ServerControler需求 */ + override fun getValue(): ParticleGroupStyle { + return this + } + + /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ + fun change(toggleMethod: ParticleGroupStyle.() -> Unit, args: Map>) { + if (client) { + return + } + toggleMethod(this) + // 发包 + ParticleStyleManager.filterVisiblePlayer(this).forEach { + val player = world!!.getPlayerByUUID(it) ?: return@forEach + CooParticlesServices.SERVER_NETWORK.send( + PacketParticleStyleS2C( + uuid, ControlType.CHANGE, args + ), player as ServerPlayer + ) + } + } + + /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ + fun change(args: Map>) { + change({}, args) + } + + open fun display(pos: Vec3, world: Level) { + if (displayed) { + return + } + displayed = true + this.displayedTime = world.gameTime + this.lastUpdatedGameTime = world.gameTime + this.pos = pos + this.world = world + this.client = world.isClientSide + if (!client) { + // 服务器只负责数据同步 不负责粒子生成 + onDisplay() + return + } + flush() + onDisplay() + } + + open fun flush() { + if (particles.isNotEmpty()) { + clear(true) + } + displayParticles() + } + + open fun toggleRelative() { + if (!client) { + return + } + val iterator = particleLocations.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val particle = entry.key + val rl = entry.value + particle.teleportTo( + pos.add( + rl.toVector() + ) + ) + } + positionDirty = false + } + + + private fun spawnSingleParticle(data: StyleData, rel: RelativeLocation) { + val displayer = data.displayerBuilder(data.uuid) + if (displayer !is ParticleDisplayer.SingleParticleDisplayer) return + val controler = ControlParticleManager.createControl(data.uuid) + controler.applyInitializedAction(data.particleHandler) + val toPos = Vec3(pos.x + rel.x, pos.y + rel.y, pos.z + rel.z) + displayer.display(toPos, world as ClientLevel) ?: return + data.particleControlerHandler(controler) + // 把粒子丢回生成列表 + particles[data.uuid] = controler + particleLocations[controler] = rel + } + + override fun tick() { + if (!displayed || !valid) { + clear(false) + return + } + val current = world!!.gameTime + if (!displayed || !valid) { + clear(false) + return + } + invokeQueue.forEach { + it(this) + } + + val iterator = particles.iterator() + while (iterator.hasNext()) { + val style = iterator.next() + when (val value = style.value) { + is Tickable<*> -> { + value.tick() + } + } + } + postInvokeQueue.forEach { + it(this) + } + this.lastUpdatedGameTime = current + } + + fun toggleScale(locations: Map) { + if (!valid) { + // remove过后 无法同步 + return + } + if (particleDefaultLength.isEmpty()) { + locations.forEach { + val uuid = it.key.uuid + particleDefaultLength[uuid] = it.value.length() + } + } + locations.forEach { + val uuid = it.key.uuid + val len = particleDefaultLength[uuid] ?: return@forEach + if (len <= 0.0) { + return@forEach + } + val value = it.value + value.multiply(len * scale / value.length()) + } + } + + open fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale can not be less than zero") + return + } + scale = new + // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 + // 多为服务端(除非有人使在Client环境创建了这个类) + if (displayed) { + toggleScaleDisplayed() + } + // 发包有效 + if (!valid) { + // remove过后 无法同步 + return + } + if (!client) { + change(mapOf("scale" to ParticleControlerDataBuffers.double(new))) + } + } + + protected open fun toggleScaleDisplayed() { + particleLocations.forEach { + val uuid = it.key.controlUUID() + val len = particleDefaultLength[uuid]!! + val value = it.value + if (len in -1e-3..1e-3) return@forEach + value.multiply(len * scale / value.length()) + } + toggleRelative() + } + + override fun spawn(world: Level, pos: Vec3) { + if (world !is ServerLevel) return + ParticleStyleManager.spawnStyle(world, pos, this) + } + + open fun preRotateTo(map: Map, to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + map.values.toList(), to, axis + ) + this.axis = to + } + + open fun preRotateAsAxis(map: Map, axis: RelativeLocation, angle: Double) { + Math3DUtil.rotateAsAxis( + map.values.toList(), axis, angle + ) + this.axis = axis + } + + open fun preRotateAsAxis(map: Map, angle: Double) { + Math3DUtil.rotateAsAxis( + map.values.toList(), axis, angle + ) + } + + private fun displayParticles() { + val locations = getCurrentFrames() + beforeDisplay(locations) + toggleScale(locations) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) + locations.forEach { + val data = it.key + val uuid = it.key.uuid + val rl = it.value + val displayer = it.key.displayerBuilder(uuid) + prepareCParticleDisplayer(displayer, locations.size) + if (displayer is ParticleDisplayer.SingleParticleDisplayer) { + val controler = ControlParticleManager.createControl(uuid) + controler.applyInitializedAction(data.particleHandler) + } + val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) + val controler = displayer.display(toPos, world as ClientLevel) ?: return@forEach + if (controler is ParticleControler) { + data.particleControlerHandler(controler) + } + registerCParticleNode(controler) + + particles[uuid] = controler + particleLocations[controler] = rl + } + } + + override fun remove(reason: RemoveReason) { + remove() + } + + open internal fun clear(valid: Boolean) { + particles.forEach { + it.value.remove() + } + particles.clear() + particleLocations.clear() + particleDefaultLength.clear() + cParticleSystems.clear() + cParticleContainers.clear() + this.valid = valid + } + + open class StyleData( + val displayerBuilder: (UUID) -> ParticleDisplayer, + ) { + val uuid: UUID = UUID.randomUUID() + var particleHandler: ControlableParticle.() -> Unit = {} + var particleControlerHandler: ParticleControler.() -> Unit = {} + fun withParticleHandler( + builder: ControlableParticle.() -> Unit + ): StyleData { + particleHandler = builder + return this + } + + fun withParticleControlerHandler( + builder: ParticleControler.() -> Unit + ): StyleData { + particleControlerHandler = builder + return this + } + } + + /** 为了提高傻逼StyleData的复用性 专门设置此类 */ + open class StyleDataBuilder() { + private var displayerBuilder: (UUID) -> ParticleDisplayer = + { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } + private val particleHandlers = mutableListOf Unit>() + private val particleControlerHandlers = mutableListOf Unit>() + fun addParticleHandler( + builder: ControlableParticle.() -> Unit + ): StyleDataBuilder { + particleHandlers.add(builder) + return this + } + + fun addParticleControlerHandler( + builder: ParticleControler.() -> Unit + ): StyleDataBuilder { + particleControlerHandlers.add(builder) + return this + } + + fun clearParticleHandlers(): StyleDataBuilder { + particleHandlers.clear() + return this + } + + fun clearParticleControlers(): StyleDataBuilder { + particleControlerHandlers.clear() + return this + } + + fun removeHandler(index: Int): StyleDataBuilder { + if (index in particleHandlers.indices) particleHandlers.removeAt(index) + return this + } + + fun removeParticleControler(index: Int): StyleDataBuilder { + if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) + return this + } + + fun displayer(builder: (UUID) -> ParticleDisplayer): StyleDataBuilder { + this.displayerBuilder = builder + return this + } + + fun build(): StyleData = StyleData(displayerBuilder) + .withParticleHandler { + particleHandlers.forEach { it() } + }.withParticleControlerHandler { + particleControlerHandlers.forEach { it() } + } + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt new file mode 100644 index 00000000..25e9c85d --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt @@ -0,0 +1,222 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper +import net.minecraft.client.particle.ParticleRenderType +import net.minecraft.world.entity.player.Player +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +/** + * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) + * 专门创建此类 + * + * 此类没有Provider 因此无法单独生成 + * + * 继承此类的目的是为这个类创建一个专属形状的子类 + * 这种子类可以拥有Provider + */ +open class ParticleShapeStyle(uuid: UUID) : + ParticleGroupStyle(64.0, uuid) { + var scaleHelper: ScaleHelper? = null + + /** + * 增加可复用性 + */ + private val displayInvokes = mutableListOf Unit>() + private val beforeDisplayInvokes = mutableListOf) -> Unit>() + private val pointBuilders = LinkedHashMap StyleData>() + + /** + * 通过判断(外层) 提供的状态工具来决定是否反转缩放 + * 在toggleDisplay执行这个方法 + */ + val reverseFunctionFromStatus: (ParticleShapeStyle, StyleStatusHelper) -> Unit = function@{ it, displayStatus -> + // 必须设置缩放工具 + if (scaleHelper == null) return@function + it.addPreTickAction { + if (it.scaleReversed && it.scaleHelper!!.isZero()) { + it.remove() + return@addPreTickAction + } + if (displayStatus.displayStatus != 2) { + return@addPreTickAction + } + if (!it.scaleReversed) { + it.scaleReversed(false) + } + } + } + var spawnAge = 0 + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + private set + + fun appendBuilder(pointsBuilder: PointsBuilder, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { + pointBuilders[pointsBuilder] = dataBuilder + return this + } + + fun appendPoint(point: RelativeLocation, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { + pointBuilders[ + PointsBuilder().also { it.addPoint(point) } + ] = dataBuilder + return this + } + + /** + * @param max 直接让scale从最大开始 + */ + fun scaleReversed(max: Boolean): ParticleShapeStyle { + scaleHelper ?: return this + scaleReversed = true + if (max) { + scaleHelper!!.toggleScale(scaleHelper!!.maxScale) + } + return this + } + + fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): ParticleShapeStyle { + scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): ParticleShapeStyle { + scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + /** + * 在display之前执行 + * 可以多次执行此方法, 不会发生方法覆盖 + */ + fun toggleOnDisplay(toggleMethod: ParticleShapeStyle.() -> Unit): ParticleShapeStyle { + displayInvokes.add(toggleMethod) + return this + } + + /** + * 在生成粒子之前执行 + */ + fun toggleBeforeDisplay(toggleMethod: ParticleShapeStyle.(Map) -> Unit): ParticleShapeStyle { + beforeDisplayInvokes.add(toggleMethod) + return this + } + + override fun beforeDisplay(styles: Map) { + beforeDisplayInvokes.forEach { it(styles) } + } + + override fun getCurrentFrames(): Map { + val res = HashMap() + pointBuilders.forEach { entry -> + res.putAll(entry.key.createWithStyleData { entry.value(it) }) + } + return res + } + + override fun isValid(): Boolean { + return valid + } + + override fun onDisplay() { + displayInvokes.forEach { + it() + } + addPreTickAction { + spawnAge++ + if (!scalePreTick || scaleHelper == null) { + return@addPreTickAction + } + + if (scaleReversed) { + scaleHelper!!.doScaleReversed() + } else { + scaleHelper!!.doScale() + } + } + } + + override fun writePacketArgs(): Map> { + return mapOf() + } + + override fun readPacketArgs(args: Map>) { + } + + fun fastRotateToPlayerView(player: Player) { + rotateToPoint(RelativeLocation.of(player.forward)) + } + + fun fastStyleData(color: Vec3, displayer: (UUID) -> ParticleDisplayer): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + } + } + + fun fastStyleData(color: Vec3, sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + } + } + + fun fastStyleData( + color: Vec3, + sheet: ParticleRenderType, + size: Float, + displayer: (UUID) -> ParticleDisplayer + ): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + } + } + + fun fastStyleData( + color: Vec3, + sheet: ParticleRenderType, + size: Float, + alpha: Float, + displayer: (UUID) -> ParticleDisplayer + ): StyleData { + return StyleData(displayer).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + this.particleAlpha = alpha + } + } + + fun fastStyleData(sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { + return StyleData(displayer).withParticleHandler { + this.textureSheet = sheet + } + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt new file mode 100644 index 00000000..dd68ba0c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt @@ -0,0 +1,259 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.CooParticlesAPI +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C +import cn.coostack.cooparticlesapi.particles.control.ControlType +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import net.minecraft.client.Minecraft +import net.minecraft.server.level.ServerPlayer +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 + +/** + * TODO 重构Style使得让自动更新默认打开(如果更新时发现不存在那就重新生成) + * + * @constructor Create empty Particle style manager + */ +@Deprecated("使用ParticleComposition") +object ParticleStyleManager { + + /** + * 服务端拥有的 -> (Server View)客户层是empty + */ + val serverViewStyles = ConcurrentHashMap() + + /** + * 玩家可以看见的 style + */ + internal val visible = ConcurrentHashMap>() + + /** + * 当前客户端可见的 (Client View)服务层是empty + */ + val clientViewStyles = ConcurrentHashMap() + + private val registerBuilders = + HashMap, ParticleStyleProvider>() + + /** + * 在ClientModInitializer 注册ParticleStyle 用于服务器同步 + */ + // Keep this as an instance method for binary compatibility with existing consumers. + fun register( + type: Class, + provider: ParticleStyleProvider + ) { + registerBuilders[type] = provider + } + + fun getBuilder(type: Class): ParticleStyleProvider? { + return registerBuilders[type] + } + + @JvmStatic + fun spawnStyle(world: Level, pos: Vec3, style: ParticleGroupStyle) { + if (world.isClientSide) { + // 生成粒子 + style.display(pos, world) + clientViewStyles[style.uuid] = style + return + } + style.display(pos, world) + serverViewStyles[style.uuid] = style + // 发送数据包 -> 包括type + world.players().filter { style.pos.distanceTo(it.position()) <= style.visibleRange }.forEach { + addStylePlayerView(it as ServerPlayer, style) + } + } + + + fun doTickClient() { + val player = Minecraft.getInstance().player ?: return + if (player.isDeadOrDying) { + clientViewStyles.clear() + return + } + val iterator = clientViewStyles.iterator() + while (iterator.hasNext()) { + val style = iterator.next().value + style.tick() + if (!style.valid) { + iterator.remove() + } + } + } + + fun doTickServer() { + val iterator = serverViewStyles.iterator() + while (iterator.hasNext()) { + val style = iterator.next().value + // 更新可见性 + upgradeVisible(style) + style.tick() + if (style.autoToggle) { + style.world!!.players().forEach { + val visibleSet = visible.getOrPut(it.uuid) { HashSet() } + // 不可见的粒子没有必要同步 + if (!visibleSet.contains(style)) { + return@forEach + } + CooParticlesServices.SERVER_NETWORK.send( + buildAutoTogglePacket(style), + it as ServerPlayer, + ) + } + } + if (!style.valid) { + filterVisiblePlayer(style).forEach { + val player = style.world!!.getPlayerByUUID(it) ?: return@forEach + removeGroupPlayerView(player as ServerPlayer, style) + visible[it]?.remove(style) + } + iterator.remove() + } + } + val playerVisibleIterator = visible.iterator() + while (playerVisibleIterator.hasNext()) { + val playerUUID = playerVisibleIterator.next().key + // 判断玩家是否在线 + val server = CooParticlesAPI.serverOrNull ?: return + val player = server.playerList.getPlayer(playerUUID) + if (player == null) { + playerVisibleIterator.remove() + } + } + } + + fun filterVisiblePlayer(group: ParticleGroupStyle): Set { + val set = HashSet() + visible.forEach { + if (group in it.value) { + set.add(it.key) + } + } + return set + } + + private fun upgradeVisible(style: ParticleGroupStyle) { + val server = CooParticlesAPI.serverOrNull ?: return + server.playerList.players.forEach { p -> + val visibleSet = visible.getOrPut(p.uuid) { HashSet() } + if (p.level().dimension() != style.world?.dimension()) { + // 世界转换 + if (style in visibleSet) { + removeGroupPlayerView(p, style) + visibleSet!!.remove(style) + } + return@forEach + } + if (p.isDeadOrDying) { + if (style in visibleSet) { + removeGroupPlayerView(p, style) + visibleSet!!.remove(style) + } + return@forEach + } + if (style.pos.distanceTo(p.position()) <= style.visibleRange) { + // 防止重复添加(发包) + if (style in visibleSet) { + return@forEach + } + addStylePlayerView(p, style) + } else { + if (style in visibleSet) { + removeGroupPlayerView(p, style) + } + // 超过范围 + visibleSet.remove(style) + } + } + } + + /** + * 在该玩家的视角中移除这些粒子 + * @param target + * @param targetGroup + */ + private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ParticleGroupStyle) { + // 发包给玩家 + val packet = PacketParticleStyleS2C( + targetGroup.uuid, + ControlType.REMOVE, mapOf() + ) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + /* + * @param target + * @param targetStyle + */ + private fun addStylePlayerView( + target: ServerPlayer, + targetStyle: ParticleGroupStyle, + ) { + // 修复一个包发送给一个玩家两次 + val visibleSet = visible.getOrPut(target.uuid) { HashSet() } + visibleSet.add(targetStyle) + // 发包给玩家 + val packet = buildCreatePacket(targetStyle, targetStyle.pos) + CooParticlesServices.SERVER_NETWORK.send(packet, target) + } + + private fun buildAutoTogglePacket( + style: ParticleGroupStyle, + ): PacketParticleStyleS2C { + + /** + * SequencedParticleStyle + * 你赢了 + * addMultiple的index 能从 7 自动同步成 0 + * + * 一看 server side 诶 index没有问题 + * 一看args 诶 index没有问题 + * 但是呢 + * 传入客户端之后啊 + * 7 变成 0 了捏 + * 不知道的以为是Packet的 CODEC被你妈吃数据包了 + */ + + return PacketParticleStyleS2C( + style.uuid, + ControlType.CHANGE, + mapOf( + *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() + ) + ) + } + + private fun buildCreatePacket(style: ParticleGroupStyle, pos: Vec3): PacketParticleStyleS2C = + PacketParticleStyleS2C( + style.uuid, + ControlType.CREATE, + mapOf( + "style_type" to ParticleControlerDataBuffers.string(style::class.java.name), + "pos" to ParticleControlerDataBuffers.vec3d(pos), + "rotate" to ParticleControlerDataBuffers.double(style.rotate), + "axis" to ParticleControlerDataBuffers.vec3d(style.axis.toVector()), + "scale" to ParticleControlerDataBuffers.double(style.scale), + "lastUpdatedGameTime" to ParticleControlerDataBuffers.long(style.lastUpdatedGameTime), + "displayedTime" to ParticleControlerDataBuffers.long(style.displayedTime), + *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() + ) + ) + + fun clearAllVisible() { + clientViewStyles.onEach { + it.value.remove() + }.clear() + } + + fun clearServer() { + serverViewStyles.clear() + visible.clear() + } +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt new file mode 100644 index 00000000..acca17be --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt @@ -0,0 +1,13 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import java.util.UUID + +@Deprecated("使用ParticleComposition") +interface ParticleStyleProvider { + /** + * 创建style + * 执行此方法的时候不要调用基本参数(因为还没初始化) + */ + fun createStyle(uuid: UUID, args: Map>): ParticleGroupStyle +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt new file mode 100644 index 00000000..d8a36a12 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt @@ -0,0 +1,288 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.particles.ParticleDisplayer +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder +import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper +import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper +import net.minecraft.client.particle.ParticleRenderType +import net.minecraft.world.entity.player.Player +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.TreeMap +import java.util.UUID +import java.util.function.Predicate + +/** + * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) + * 专门创建此类 + * + * 无法单独生成 + * 继承此类的目的是为这个类创建一个专属形状的子类 + * client属性始终为true + */ +class SequencedParticleShapeStyle(uuid: UUID) : + SequencedParticleStyle(64.0, uuid) { + var scaleHelper: ScaleHelper? = null + + private val displayInvokes = mutableListOf Unit>() + private val beforeDisplayInvokes = + mutableListOf) -> Unit>() + + private val pointBuilders = LinkedHashMap SortedStyleData>() + + /** + * 用于控制粒子的播放顺序 + * 如果满足 animationConditions[animationIndex].first的条件 + * 则会执行 addMultiple(second (second > 0)) + * 或者 removeMultiple(second (second < 0) + * 符合条件后 animationIndex 会递增 + * 直到到达边界 + */ + private val animationConditions = ArrayList, Int>>() + var animationIndex = 0 + private set + + /** + * 通过判断(外层) 提供的状态工具来决定是否反转缩放 + * 在toggleDisplay执行这个方法 + */ + val reverseFunctionFromStatus: (SequencedParticleShapeStyle, StyleStatusHelper) -> Unit = + function@{ it, displayStatus -> + // 必须设置缩放工具 + if (scaleHelper == null) return@function + it.addPreTickAction { + if (it.scaleReversed && it.scaleHelper!!.isZero()) { + it.remove() + return@addPreTickAction + } + if (displayStatus.displayStatus != 2) { + return@addPreTickAction + } + if (!it.scaleReversed) { + it.scaleReversed(false) + } + } + } + var spawnAge = 0 + + /** + * 设置为true时 会利用scaleHelper 每tick增长一点 + */ + var scalePreTick = false + private set + + /** + * 设置为true时 利用scaleHelper 每tick减弱一点 + */ + var scaleReversed = false + private set + + fun appendBuilder( + pointsBuilder: PointsBuilder, + dataBuilder: (RelativeLocation) -> SortedStyleData + ): SequencedParticleShapeStyle { + pointBuilders[pointsBuilder] = dataBuilder + return this + } + + fun appendPoint( + point: RelativeLocation, + dataBuilder: (RelativeLocation) -> SortedStyleData + ): SequencedParticleShapeStyle { + pointBuilders[ + PointsBuilder().also { it.addPoint(point) } + ] = dataBuilder + return this + } + + /** + * @param predicate 设置添加/删除动画的要求 + * @param add 向下的个数 + */ + fun appendAnimateCondition( + predicate: Predicate, + add: Int + ): SequencedParticleShapeStyle { + animationConditions.add(predicate to add) + return this + } + + /** + * @param max 直接让scale从最大开始 + */ + fun scaleReversed(max: Boolean): SequencedParticleShapeStyle { + scaleHelper ?: return this + scaleReversed = true + if (max) { + scaleHelper!!.toggleScale(scaleHelper!!.maxScale) + } + return this + } + + fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): SequencedParticleShapeStyle { + scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + fun loadScaleHelperBezierValue( + minScale: Double, + maxScale: Double, + scaleTick: Int, + c1: RelativeLocation, + c2: RelativeLocation + ): SequencedParticleShapeStyle { + scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) + scalePreTick = true + scaleHelper!!.loadControler(this) + return this + } + + /** + * 在display之前执行 + */ + fun toggleOnDisplay(toggleMethod: SequencedParticleShapeStyle.() -> Unit): SequencedParticleShapeStyle { + displayInvokes.add(toggleMethod) + return this + } + + /** + * 在生成粒子之前执行 + */ + fun toggleBeforeDisplay(toggleMethod: SequencedParticleShapeStyle.(SortedMap) -> Unit): SequencedParticleShapeStyle { + beforeDisplayInvokes.add(toggleMethod) + return this + } + + + override fun beforeDisplay(styles: SortedMap) { + beforeDisplayInvokes.forEach { + it(styles) + } + } + + + override fun onDisplay() { + displayInvokes.forEach { it() } + addPreTickAction { + if (animationIndex >= animationConditions.size) { + return@addPreTickAction + } + val (predicate, add) = animationConditions[animationIndex] + if (predicate.test(this@SequencedParticleShapeStyle)) { + if (add > 0) { + addMultiple(add) + } else { + removeMultiple(add) + } + animationIndex++ + } + } + addPreTickAction { + spawnAge++ + if (!scalePreTick || scaleHelper == null) { + return@addPreTickAction + } + + if (scaleReversed) { + scaleHelper!!.doScaleReversed() + } else { + scaleHelper!!.doScale() + } + } + } + + private var count = -1 + override fun getParticlesCount(): Int { + // 调用此方法比初始化count早 + if (count == -1 || count == 0) { + count = getCurrentFramesSequenced().size + } + return count + } + + override fun getCurrentFramesSequenced(): SortedMap { + val res = TreeMap() + pointBuilders.forEach { entry -> + res.putAll(entry.key.createWithSequencedStyleData { it, order -> + entry.value(it) + }) + } + return res + } + + override fun writePacketArgsSequenced(): Map> { + return mapOf() + } + + override fun readPacketArgsSequenced(args: Map>) { + } + + + fun fastRotateToPlayerView(player: Player) { + rotateToPoint(RelativeLocation.of(player.forward)) + } + + fun fastStyleData(order: Int, color: Vec3, displayer: (UUID) -> ParticleDisplayer): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + color: Vec3, + sheet: ParticleRenderType, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + color: Vec3, + sheet: ParticleRenderType, + size: Float, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + color: Vec3, + sheet: ParticleRenderType, + size: Float, + alpha: Float, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) + this.textureSheet = sheet + this.size = size + this.particleAlpha = alpha + } as SortedStyleData + } + + fun fastStyleData( + order: Int, + sheet: ParticleRenderType, + displayer: (UUID) -> ParticleDisplayer + ): SortedStyleData { + return SortedStyleData(displayer, order).withParticleHandler { + this.textureSheet = sheet + } as SortedStyleData + } +} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt new file mode 100644 index 00000000..f9825d8c --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt @@ -0,0 +1,551 @@ +package cn.coostack.cooparticlesapi.network.particle.style + +import cn.coostack.cooparticlesapi.CooParticlesConstants +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer +import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers +import cn.coostack.cooparticlesapi.particles.ControlableParticle +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.impl.ControlableEndRodEffect +import cn.coostack.cooparticlesapi.utils.Math3DUtil +import cn.coostack.cooparticlesapi.utils.MathDataUtil +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.SortedMap +import java.util.UUID +import kotlin.collections.toList +import kotlin.math.PI + + +/** + * 几个免除手动书写 SequencedParticleStyle的方法 + * + * 在这里不建议使用 autoToggle方法 会导致一些奇奇怪怪的问题 (原因未知) 可能是 部分index同步错误导致 + */ +@Deprecated("使用ParticleComposition") +abstract class SequencedParticleStyle(visibleRange: Double = 32.0, uuid: UUID = UUID.randomUUID()) : + ParticleGroupStyle(visibleRange, uuid) { + companion object { + const val CREATE_PARTICLE = 1 + const val DELETE_PARTICLE = 0 + } + + + class SortedStyleData(displayerBuilder: (UUID) -> ParticleDisplayer, val order: Int) : + StyleData(displayerBuilder), Comparable { + override fun compareTo(other: SortedStyleData): Int { + return order - other.order + } + } + + + class SortedStyleDataBuilder { + private var displayerBuilder: (UUID) -> ParticleDisplayer = + { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } + private val particleHandlers = mutableListOf Unit>() + private val particleControlerHandlers = mutableListOf Unit>() + fun addParticleHandler( + builder: ControlableParticle.() -> Unit + ): SortedStyleDataBuilder { + particleHandlers.add(builder) + return this + } + + fun addParticleControlerHandler( + builder: ParticleControler.() -> Unit + ): SortedStyleDataBuilder { + particleControlerHandlers.add(builder) + return this + } + + fun clearParticleHandlers(): SortedStyleDataBuilder { + particleHandlers.clear() + return this + } + + fun clearParticleControlers(): SortedStyleDataBuilder { + particleControlerHandlers.clear() + return this + } + + fun removeHandler(index: Int): SortedStyleDataBuilder { + if (index in particleHandlers.indices) particleHandlers.removeAt(index) + return this + } + + fun removeParticleControler(index: Int): SortedStyleDataBuilder { + if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) + return this + } + + fun displayer(builder: (UUID) -> ParticleDisplayer): SortedStyleDataBuilder { + this.displayerBuilder = builder + return this + } + + fun build(order: Int): SortedStyleData = SortedStyleData(displayerBuilder, order) + .withParticleHandler { + particleHandlers.forEach { it() } + }.withParticleControlerHandler { + particleControlerHandlers.forEach { it() } + } as SortedStyleData + } + + /** + * 在服务器处是0 size的大小 client用于存储某个粒子/粒子组 的展示情况 + * + * 用于彻底解除强制重写的烦恼 + * + * FIXED 修复多余的内存占用 + */ + val displayedStatus: LongArray by lazy { + val count = getParticlesCount() + var page = count / 64 + if (count % 64 > 0) { + page++ + } + LongArray(page) + } + + + /** 统计对应的粒子顺序 (uuid) */ + protected var sequencedParticles = ArrayList>() + private set + + + var displayedParticleCount = 0 + private set(value) { + field = value.coerceAtLeast(0) + } + + /** + * 此参数所指向的状态为false + * + * 服务器可用 不会同步到客户端 + */ + var particleLinkageDisplayCurrentIndex = 0 + private set + + /** + * @return 当前粒子样式的样式个数 (客户端执行getCurrentFramesSequenced返回的集合长度) 请手动计算粒子个数 + * 或者维护一个变量 支持服务器和客户端同时生成 请勿直接在服务器中调用 getCurrentFramesSequenced().size + * 否则会导致服务器崩溃 + */ + abstract fun getParticlesCount(): Int + abstract fun getCurrentFramesSequenced(): SortedMap + abstract fun writePacketArgsSequenced(): Map> + abstract fun readPacketArgsSequenced(args: Map>) + + /** 服务器发包 -> index_status_change -> intArray index, status */ + fun addSingle() { + if (client) { + // 适配在纯客户端页面的添加 + // 由于和服务器无关所以大多数参数无需设置 + if (particleLinkageDisplayCurrentIndex >= getParticlesCount()) { + return + } + toggleFromStatus(particleLinkageDisplayCurrentIndex++, true) + return + } + displayedParticleCount++ + if (getStatus(particleLinkageDisplayCurrentIndex)) { + return + } + setStatus(particleLinkageDisplayCurrentIndex, true) + // 处理服务器 + // 发包 + val args = buildChangeSingleStatusArgs( + particleLinkageDisplayCurrentIndex++, + true + ) + change({}, mapOf(args)) + } + + /** + * 服务器发包 -> indexes_status_change_arg -> intArray indexes + * indexes_status_change_method -> intArray status method + */ + fun addMultiple(count: Int) { + if (client) { + repeat(count) { + addSingle() + } + return + } + if (count <= 0) { + return + } + if (count == 1) { + addSingle() + return + } + displayedParticleCount += count + // 处理服务器 + // 发包 + val indexes = particleLinkageDisplayCurrentIndex..> { + return mapOf( + "indexes_status_change_arg" to ParticleControlerDataBuffers.intArray(indexes), + "indexes_status_change_status" to ParticleControlerDataBuffers.intArray( + intArrayOf(if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount) + ) + ) + } + + private fun buildChangeSingleStatusArgs( + index: Int, + status: Boolean, + ): Pair> { + return "index_status_change" to ParticleControlerDataBuffers.intArray( + intArrayOf( + index, + if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount + ) + ) + } + + open fun beforeDisplay(styles: SortedMap) {} + fun toggleScale(locations: SortedMap) { + super.toggleScale(locations.toMap()) + } + + override fun scale(new: Double) { + if (new < 0.0) { + CooParticlesConstants.logger.error("scale can not be less than zero") + return + } + scale = new + if (displayed) { + toggleScaleDisplayed() + } + } + + override fun display(pos: Vec3, world: Level) { + if (displayed) { + return + } + displayed = true + this.pos = pos + this.world = world + this.client = world.isClientSide + if (!client) { + // 服务器只负责数据同步 不负责粒子生成 + onDisplay() + return + } + flush() + toggleDataStatus() + onDisplay() + } + + /** 清空粒子生成状态 重新生成粒子 */ + override fun flush() { + if (particles.isNotEmpty()) { + clear(true) + clearStatus() + } + displayParticles() + } + + override fun clear(valid: Boolean) { + sequencedParticles.clear() + super.clear(valid) + } + + /** 新的参数用于传输 粒子启用顺序 */ + override fun writePacketArgs(): Map> { + return mapOf( + "status" to ParticleControlerDataBuffers.longArray(displayedStatus), + "displayed_particle_count" to ParticleControlerDataBuffers.int(displayedParticleCount), + *writePacketArgsSequenced().map { it.key to it.value }.toTypedArray() + ) + } + + override fun readPacketArgs(args: Map>) { + args["status"]?.let { + val array = it.loadedValue as LongArray + System.arraycopy(array, 0, displayedStatus, 0, array.size) + } + args["displayed_particle_count"]?.let { displayedParticleCount = it.loadedValue!! as Int } + readPacketArgsSequenced(args) + args["index_status_change"]?.let { + val array = it.loadedValue as IntArray + + /** @see particleLinkageDisplayCurrentIndex */ + val index = array[0] + val status = array[1] == CREATE_PARTICLE + this.displayedParticleCount = array[2] + changeSingleStatus(index, status) + } + + args["indexes_status_change_arg"]?.let { + val indexes = it.loadedValue as IntArray + val methodArray = args["indexes_status_change_status"]!!.loadedValue as IntArray + val status = methodArray[0] == CREATE_PARTICLE + this.displayedParticleCount = methodArray[1] + changeParticlesStatus(indexes, status) + } + } + + fun displayParticles() { + val locations = getCurrentFramesSequenced() + beforeDisplay(locations) + toggleScale(locations) + sequencedParticles.addAll(locations.toList()) + Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) + } + + override fun rotateAsAxis(angle: Double) { + Math3DUtil.rotateAsAxis( + sequencedParticles.map { it.second }.toList(), axis, angle + ) + this.rotate += angle + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client && !autoToggle) { + // 同步到其他客户端 + change( + mapOf( + "rotate_angle" to ParticleControlerDataBuffers.double(angle) + ) + ) + } + } + + override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { + Math3DUtil.rotateAsAxis( + sequencedParticles.map { it.second }.toList(), axis, angle + ) + Math3DUtil.rotatePointsToPoint( + sequencedParticles.map { it.second }.toList(), to, axis + ) + axis = to + this.rotate += angle + if (this.rotate >= 2 * PI) { + this.rotate -= 2 * PI + } + toggleRelative() + if (!client && !autoToggle) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to), + "rotate_angle" to ParticleControlerDataBuffers.double(angle) + ) + ) + } + } + + override fun rotateToPoint(to: RelativeLocation) { + Math3DUtil.rotatePointsToPoint( + sequencedParticles.map { it.second }.toList(), to, axis + ) + axis = to + toggleRelative() + if (!client && !autoToggle) { + // 同步到其他客户端 + change( + mapOf( + "rotate_to" to ParticleControlerDataBuffers.relative(to) + ) + ) + } + } + + final override fun toggleScaleDisplayed() { + sequencedParticles.forEach { + val uuid = it.first.uuid + val len = particleDefaultLength[uuid]!! + val value = it.second + if (len in -1e-3..1e-3) return@forEach + value.multiply(len * scale / value.length()) + } + toggleRelative() + } + + private fun createWithIndex(index: Int) { + if (!client) { + return + } + + if (index !in 0..sequencedParticles.size - 1) { + return + } + + val (data, rl) = sequencedParticles[index] + val uuid = data.uuid + + val displayer = data.displayerBuilder(uuid) + prepareCParticleDisplayer(displayer, sequencedParticles.size) + if (displayer is ParticleDisplayer.SingleParticleDisplayer) { + val controler = ControlParticleManager.createControl(uuid) + controler.applyInitializedAction(data.particleHandler) + } + val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) + val controler = displayer.display(toPos, world as ClientLevel) ?: return + if (controler is ParticleControler) { + data.particleControlerHandler(controler) + } + registerCParticleNode(controler) + particles[uuid] = controler + particleLocations[controler] = rl + } + + private fun toggleFromStatus(index: Int, status: Boolean) { + if (index >= sequencedParticles.size && client || index > getParticlesCount()) return + if (index < 0) return + if (!client) return + if (status) { + createWithIndex(index) + } else { + val uuid = sequencedParticles[index].first.uuid + val particle = particles[uuid] ?: return + unregisterCParticleNode(particle) + particle.remove() + particles.remove(uuid) + particleLocations.remove(particle) + } + setStatus(index, status) + } + + private fun setStatus(index: Int, status: Boolean) { + val page = MathDataUtil.getStoragePageLong(index) + val container = displayedStatus[page] + val bit = MathDataUtil.getStorageWithBitLong(index) + displayedStatus[page] = MathDataUtil.setStatusLong(container, bit, status) + } + + private fun getStatus(index: Int): Boolean { + val page = MathDataUtil.getStoragePageLong(index) + val container = displayedStatus[page] + val bit = MathDataUtil.getStorageWithBitLong(index) + return MathDataUtil.getStatusLong(container, bit) == 1 + } + + /** 在同步了data状态后 执行生成已经生成的 客户端执行 */ + private fun toggleDataStatus() { + if (!client) { + return + } + + if (displayedStatus.isEmpty()) return + if (!displayed) return + displayedStatus.forEachIndexed { page, container -> + for (bit in 1..64) { + val index = page * 64 + bit - 1 + if (index >= sequencedParticles.size) { + break + } + val status = MathDataUtil.getStatusLong(container, bit) + toggleFromStatus(index, status == 1) + } + + } + } + + private fun clearStatus() { + for (i in displayedStatus.indices) { + displayedStatus[i] = 0L + } + } + + final override fun getCurrentFrames(): Map { + return mapOf() + } + + + final override fun beforeDisplay(styles: Map) {} +} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt new file mode 100644 index 00000000..428667b6 --- /dev/null +++ b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt @@ -0,0 +1,111 @@ +package cn.coostack.cooparticlesapi.network.particle.util + +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C +import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C +import cn.coostack.cooparticlesapi.platform.CooParticlesServices +import cn.coostack.cooparticlesapi.utils.RelativeLocation +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.server.level.ServerLevel +import net.minecraft.world.phys.Vec3 + +object ServerParticleUtil { + + @JvmStatic + fun spawnBatch( + type: ParticleOptions, + world: ServerLevel, + positions: Iterable, + velocity: Vec3, + range: Double, + ) { + val allPositions = positions.toList() + if (allPositions.isEmpty()) return + world.players().forEach { player -> + val visiblePositions = allPositions.filter { player.position().distanceTo(it) <= range } + visiblePositions.chunked(PacketParticleBatchS2C.MAX_PARTICLES).forEach { batch -> + CooParticlesServices.SERVER_NETWORK.send(PacketParticleBatchS2C(type, batch, velocity), player) + } + } + } + + @JvmStatic + fun spawnBatch( + type: ParticleOptions, + world: ServerLevel, + positions: Iterable, + velocity: Vec3, + ) { + positions.toList() + .chunked(PacketParticleBatchS2C.MAX_PARTICLES) + .filter { it.isNotEmpty() } + .forEach { batch -> + val packet = PacketParticleBatchS2C(type, batch, velocity) + world.players().forEach { CooParticlesServices.SERVER_NETWORK.send(packet, it) } + } + } + /** + * 使用minecraft的 spawnParticle方法 + * 可能无法设置粒子移动方向 + */ + fun spawnSingle( + type: ParticleOptions, + world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int + ) { + world.players().forEach { + world.sendParticles( + it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed + ) + } + } + + /** + * 使用minecraft的 spawnParticle方法 + * 可能无法设置粒子移动方向 + */ + fun spawnSingle( + type: ParticleOptions, + world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int, range: Double + ) { + world.players().forEach { + if (it.position().distanceTo(pos) > range) { + return@forEach + } + world.sendParticles( + it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed + ) + } + } + + /** + * 使用CooParticleAPI的 spawnParticle方法 + */ + fun spawnSingle( + type: ParticleOptions, + world: ServerLevel, + pos: RelativeLocation, + velocity: RelativeLocation, + range: Double + ) { + spawnSingle(type, world, pos.toVector(), velocity.toVector(), range) + } + + fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: RelativeLocation, velocity: RelativeLocation) { + spawnSingle(type, world, pos.toVector(), velocity.toVector()) + } + + fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3, range: Double) { + world.players().forEach { + if (it.position().distanceTo(pos) > range) { + return@forEach + } + CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) + } + } + + fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3) { + world.players().forEach { + CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) + } + } + +} From 1271d2148e42217dce02657e6e9c4c6fa420c21b Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:10:01 +0800 Subject: [PATCH 02/41] ci: trigger workflow From d4cc6d586f7f0b9a5c7d012bea113e0852cf35e4 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:12:14 +0800 Subject: [PATCH 03/41] ci: trigger build From 603faac45ae569420595127164097bb6d91af7c5 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:26:00 +0800 Subject: [PATCH 04/41] ci: retrigger build From 1365ad6c79f13e255124decea64105a927dc8bbe Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:27:22 +0800 Subject: [PATCH 05/41] ci: force trigger From d9c3767cdd4926aef9c4d0e238d131af897f0d43 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:34:15 +0800 Subject: [PATCH 06/41] ci: add workflow_dispatch trigger to gradle workflow --- .github/workflows/gradle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8ed1b0a4..735fe6c6 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 From bdd13e8dd49d49e1de418aea26b496336dcde5d0 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:53:08 +0800 Subject: [PATCH 07/41] fix(common): add missing stub classes for compilation - Add CooIrisRenderState, CParticleForceResourceTable, CParticleCommandGlBuffer - Add CParticleForceResource, CParticleForceResourceRegistry - Add CParticleFluidResource, CParticleTextureResource These classes are referenced but missing from the multiplatform branch. --- .../cooparticlesapi/compat/iris/CooIrisRenderState.kt | 5 +++++ .../cparticle/compat/CParticleFluidResource.kt | 3 +++ .../cparticle/compat/CParticleTextureResource.kt | 3 +++ .../cparticle/force/CParticleForceResource.kt | 6 ++++++ .../cparticle/force/CParticleForceResourceRegistry.kt | 11 +++++++++++ .../cparticle/force/CParticleForceResourceTable.kt | 6 ++++++ .../cparticle/storage/CParticleCommandGlBuffer.kt | 6 ++++++ 7 files changed, 40 insertions(+) create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceRegistry.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt 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..145da35e --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt @@ -0,0 +1,5 @@ +package cn.coostack.cooparticlesapi.compat.iris + +object CooIrisRenderState { + fun snapshot(): Map = emptyMap() +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt new file mode 100644 index 00000000..6ee93e9b --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt @@ -0,0 +1,3 @@ +package cn.coostack.cooparticlesapi.cparticle.compat + +class CParticleFluidResource(val id: String) diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt new file mode 100644 index 00000000..0162fdfc --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt @@ -0,0 +1,3 @@ +package cn.coostack.cooparticlesapi.cparticle.compat + +class CParticleTextureResource(val id: String) 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..40c14eac --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +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..e5d6b41e --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +class CParticleForceResourceTable { + fun resolve(resource: CParticleForceResource): Any? = null + fun clear() {} +} 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..e32deb50 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt @@ -0,0 +1,6 @@ +package cn.coostack.cooparticlesapi.cparticle.storage + +class CParticleCommandGlBuffer { + fun upload(data: ByteArray) {} + fun bind() {} +} From 4cf79f33eb48cf83b3402cc9c84641abf26a5fc1 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 16:53:38 +0800 Subject: [PATCH 08/41] ci: trigger build after adding missing classes From 14cbff4645d67f9e2434968f3dc7277be8c51bcf Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 17:09:17 +0800 Subject: [PATCH 09/41] fix(common): add missing stubs for compilation - Add PacketByteBuf interface replacing MC 1.20.5+ FriendlyByteBuf - Fix CParticleCommandGlBuffer upload signature (FloatArray + count) - Add textureBinding/fluidBinding/slotFor/textureBindings/fluidBindings to CParticleForceResourceTable - Add MAX_TEXTURE_RESOURCES to CParticleForceResourceTable - Add bindCompute/resetCompute to CParticleTextureResource and CParticleFluidResource - Add IrisSnapshot interface with all missing getters to CooIrisRenderState - Add clearFinalColor() to CooIrisRenderState --- .../compat/iris/CooIrisRenderState.kt | 36 ++++++++++++++++++- .../compat/CParticleFluidResource.kt | 5 ++- .../compat/CParticleTextureResource.kt | 5 ++- .../force/CParticleForceResourceTable.kt | 9 +++++ .../storage/CParticleCommandGlBuffer.kt | 8 +++-- .../network/packet/api/PacketByteBuf.kt | 18 ++++++++++ 6 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/PacketByteBuf.kt 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 index 145da35e..f8da2927 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt @@ -1,5 +1,39 @@ package cn.coostack.cooparticlesapi.compat.iris object CooIrisRenderState { - fun snapshot(): Map = emptyMap() + fun snapshot(): IrisSnapshot = EmptySnapshot + fun clear() {} + fun clearFinalColor() {} + + 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/CParticleFluidResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt index 6ee93e9b..53956c3e 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt @@ -1,3 +1,6 @@ package cn.coostack.cooparticlesapi.cparticle.compat -class CParticleFluidResource(val id: String) +class CParticleFluidResource(val id: String) { + fun bindCompute(index: Int) {} + fun resetCompute() {} +} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt index 0162fdfc..6e171532 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt @@ -1,3 +1,6 @@ package cn.coostack.cooparticlesapi.cparticle.compat -class CParticleTextureResource(val id: String) +class CParticleTextureResource(val id: String) { + fun bindCompute(index: Int) {} + fun resetCompute() {} +} 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 index e5d6b41e..1f47d01e 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt @@ -1,6 +1,15 @@ package cn.coostack.cooparticlesapi.cparticle.force 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): Any? = null + fun fluidBinding(slot: Int): Any? = null + fun textureBindings(): List = emptyList() + fun fluidBindings(): List = emptyList() fun clear() {} } 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 index e32deb50..93abf822 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/storage/CParticleCommandGlBuffer.kt @@ -1,6 +1,10 @@ package cn.coostack.cooparticlesapi.cparticle.storage class CParticleCommandGlBuffer { - fun upload(data: ByteArray) {} - fun bind() {} + 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/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 +} From e4f1890801869034fbef65979f02c414212b884a Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 17:21:08 +0800 Subject: [PATCH 10/41] fix(common): move resource stubs to cparticle.force, add binding interfaces - Move CParticleTextureResource and CParticleFluidResource from cparticle.compat to cparticle.force - Add CParticleTextureBinding/CParticleFluidBinding interfaces with sampleTexture/sampleFluid - Fix CParticleForceResourceTable.textureBinding/fluidBinding return types - Add NullTextureBinding/NullFluidBinding no-op implementations - Fix type inference in CParticleGpuSimulator emptyList calls --- .../compat/CParticleFluidResource.kt | 6 ------ .../compat/CParticleTextureResource.kt | 6 ------ .../cparticle/force/CParticleFluidResource.kt | 9 +++++++++ .../force/CParticleForceResourceTable.kt | 20 +++++++++++++++++-- .../force/CParticleTextureResource.kt | 9 +++++++++ .../simulate/CParticleGpuSimulator.kt | 6 ++++-- 6 files changed, 40 insertions(+), 16 deletions(-) delete mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt delete mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt deleted file mode 100644 index 53956c3e..00000000 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleFluidResource.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.cparticle.compat - -class CParticleFluidResource(val id: String) { - fun bindCompute(index: Int) {} - fun resetCompute() {} -} diff --git a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt deleted file mode 100644 index 6e171532..00000000 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/compat/CParticleTextureResource.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.cparticle.compat - -class CParticleTextureResource(val id: String) { - fun bindCompute(index: Int) {} - fun resetCompute() {} -} 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..13649443 --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceResourceTable.CParticleFluidBinding + +class CParticleFluidResource(val id: String) : 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/CParticleForceResourceTable.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt index 1f47d01e..4cd5a9ab 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResourceTable.kt @@ -1,5 +1,13 @@ 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 @@ -7,9 +15,17 @@ class CParticleForceResourceTable { fun resolve(resource: CParticleForceResource): Any? = null fun slotFor(resource: CParticleTextureResource): Int = -1 fun slotFor(resource: CParticleFluidResource): Int = -1 - fun textureBinding(slot: Int): Any? = null - fun fluidBinding(slot: Int): Any? = null + 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..c73f424d --- /dev/null +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt @@ -0,0 +1,9 @@ +package cn.coostack.cooparticlesapi.cparticle.force + +import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceResourceTable.CParticleTextureBinding + +class CParticleTextureResource(val id: String) : 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 { From 1d25662438cd6974e0ae21f52d7c5e1a011e9170 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 17:37:48 +0800 Subject: [PATCH 11/41] fix(common): fix package for resource stubs, add inheritance - Move CParticleTextureResource and CParticleFluidResource to cparticle.force package - Make them extend CParticleForceResource to fix ArrayList type compatibility - Implement CParticleTextureBinding/CParticleFluidBinding interfaces directly - Fix wrong nested-class import syntax in resource stubs - Use explicit local variable in CParticleEmitterBridge to avoid Kotlin synthetic bridge method issue - Fix emptyList type inference in CParticleGpuSimulator --- .../cparticle/compat/CParticleEmitterBridge.kt | 10 ++++++---- .../cparticle/force/CParticleFluidResource.kt | 4 +--- .../cparticle/force/CParticleTextureResource.kt | 4 +--- 3 files changed, 8 insertions(+), 10 deletions(-) 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 index 13649443..2c9a6966 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleFluidResource.kt @@ -1,8 +1,6 @@ package cn.coostack.cooparticlesapi.cparticle.force -import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceResourceTable.CParticleFluidBinding - -class CParticleFluidResource(val id: String) : CParticleFluidBinding { +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/CParticleTextureResource.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt index c73f424d..3b9643d4 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleTextureResource.kt @@ -1,8 +1,6 @@ package cn.coostack.cooparticlesapi.cparticle.force -import cn.coostack.cooparticlesapi.cparticle.force.CParticleForceResourceTable.CParticleTextureBinding - -class CParticleTextureResource(val id: String) : CParticleTextureBinding { +class CParticleTextureResource(id: String) : CParticleForceResource(id), CParticleTextureBinding { override fun sampleTexture(x: Double, y: Double, z: Double, out: FloatArray) {} fun bindCompute(index: Int) {} fun resetCompute() {} From 79a965f95ca71b61f564531d36ec7e1191a7ec84 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 17:47:33 +0800 Subject: [PATCH 12/41] fix(common): make CParticleForceResource open for extension --- .../cooparticlesapi/cparticle/force/CParticleForceResource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 40c14eac..2230fadc 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/force/CParticleForceResource.kt @@ -1,6 +1,6 @@ package cn.coostack.cooparticlesapi.cparticle.force -class CParticleForceResource(val id: String) { +open class CParticleForceResource(val id: String) { override fun equals(other: Any?): Boolean = other is CParticleForceResource && other.id == id override fun hashCode(): Int = id.hashCode() } From e629a551b1f6a1fcb14941d8dae70f840aa5eef2 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 18:11:57 +0800 Subject: [PATCH 13/41] fix(common): add missing IrisRenderState methods for Java mixin - Add beginFrame, captureSceneDepth, captureTerrainDepth, captureNoHandDepth, captureFinalColor, clear, clearFinalColor to CooIrisRenderState stub - Import Iris types (GlFramebuffer, DepthTexture, RenderTargets) for method signatures --- .../cooparticlesapi/compat/iris/CooIrisRenderState.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 index f8da2927..18c4868e 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt @@ -1,9 +1,18 @@ package cn.coostack.cooparticlesapi.compat.iris +import net.irisshaders.iris.gl.framebuffer.GlFramebuffer +import net.irisshaders.iris.targets.DepthTexture +import net.irisshaders.iris.targets.RenderTargets + object CooIrisRenderState { - fun snapshot(): IrisSnapshot = EmptySnapshot + fun beginFrame() {} + fun captureSceneDepth(depth: DepthTexture?, width: Int, height: Int) {} + fun captureTerrainDepth(textureId: Int, width: Int, height: Int) {} + fun captureNoHandDepth(textureId: Int, width: Int, height: Int) {} + fun captureFinalColor(colorAttachment0: Any?, framebufferId: Int, width: Int, height: Int) {} fun clear() {} fun clearFinalColor() {} + fun snapshot(): IrisSnapshot = EmptySnapshot object EmptySnapshot : IrisSnapshot { override fun terrainDepthTextureId(): Int = 0 From 5cb04824bfcb001b2a3b3a079eda9ec967ef183e Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 18:27:37 +0800 Subject: [PATCH 14/41] fix(common): match CooIrisRenderState signatures to Java mixin - Add @JvmStatic to all methods so Java mixin can call them statically - Fix captureSceneDepth signature to accept DepthTexture? instead of Int - Fix captureFinalColor signature to accept Any? for colorAttachment0 - Add missing import for DepthTexture, GlFramebuffer, RenderTargets --- .../compat/iris/CooIrisRenderState.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 index 18c4868e..c21b2b1e 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt @@ -1,17 +1,33 @@ 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(depth: DepthTexture?, 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 { From ea37b33af25a06158aee9d408c6f5bac44b790bd Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 18:43:25 +0800 Subject: [PATCH 15/41] fix(common): create missing FinalPassRendererAccessor mixin, fix Iris signatures - Create FinalPassRendererAccessor.java mixin accessor for FinalPassRenderer.getColorHolder() - Fix captureSceneDepth signature to accept Int (texture ID) instead of DepthTexture - RenderTargets.getDepthTexture() returns int, not DepthTexture object --- .../mixin/compat/iris/FinalPassRendererAccessor.java | 12 ++++++++++++ .../compat/iris/CooIrisRenderState.kt | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java 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/compat/iris/CooIrisRenderState.kt b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt index c21b2b1e..58bd65ab 100644 --- a/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt +++ b/common/src/main/kotlin/cn/coostack/cooparticlesapi/compat/iris/CooIrisRenderState.kt @@ -10,7 +10,7 @@ object CooIrisRenderState { fun beginFrame() {} @JvmStatic - fun captureSceneDepth(depth: DepthTexture?, width: Int, height: Int) {} + fun captureSceneDepth(depthTextureId: Int, width: Int, height: Int) {} @JvmStatic fun captureTerrainDepth(textureId: Int, width: Int, height: Int) {} From 43ecccbb105b74a8388539b65385773b3497a1bc Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 18:59:55 +0800 Subject: [PATCH 16/41] fix(neoforge): remove duplicate network files already in common The upstream multiplatform branch only has networking code in common/src/main/kotlin/... and platform-specific overrides in neoforge/src/main/kotlin/.../platform/. The Forge port commit incorrectly copied all network/ and network/buffer/ files to neoforge with the same package names, causing Redeclaration errors. --- .../network/buffer/BooleanControlerBuffer.kt | 40 - .../network/buffer/CharControlerBuffer.kt | 34 - .../network/buffer/DoubleControlerBuffer.kt | 33 - .../network/buffer/EmptyControlerBuffer.kt | 36 - .../network/buffer/FloatControlerBuffer.kt | 37 - .../network/buffer/IntArrayControlerBuffer.kt | 45 - .../network/buffer/IntControlerBuffer.kt | 35 - .../buffer/LongArrayControlerBuffer.kt | 46 - .../network/buffer/LongControlerBuffer.kt | 34 - .../buffer/NestedBuffersControlerBuffer.kt | 71 - .../buffer/ParticleControlerDataBuffer.kt | 32 - .../buffer/ParticleControlerDataBuffers.kt | 184 --- .../buffer/RelativeLocationControlerBuffer.kt | 36 - .../network/buffer/ShortControlerBuffer.kt | 33 - .../network/buffer/StringControlerBuffer.kt | 33 - .../network/buffer/UUIDControlerBuffer.kt | 33 - .../network/buffer/Vec3dControlerBuffer.kt | 41 - .../network/packet/api/ClientContext.kt | 33 - .../packet/api/CooClientPacketManager.kt | 250 ---- .../network/packet/api/CooPacket.kt | 56 - .../network/packet/api/CooPacketKind.kt | 24 - .../packet/api/CooServerPacketManager.kt | 349 ----- .../network/packet/api/ServerContext.kt | 33 - .../api/envelope/CooPacketEnvelopeC2S.kt | 52 - .../api/envelope/CooPacketEnvelopeS2C.kt | 58 - .../packet/client/PacketKeyActionC2S.kt | 56 - .../listener/ClientCameraShakeHandler.kt | 80 -- .../listener/ClientClearStateHandler.kt | 13 - .../listener/ClientDataHolderPacketHandler.kt | 15 - .../ClientDisplayEntityPacketHandler.kt | 50 - .../ClientDisplayEntityStateHandler.kt | 19 - .../ClientKeyBindingCountdownHandler.kt | 15 - .../ClientParticleCompositionHandler.kt | 89 -- .../ClientParticleCompositionRotateHandler.kt | 18 - .../ClientParticleCompositionStateHandler.kt | 20 - .../ClientParticleEmittersPacketHandler.kt | 75 -- .../ClientParticleGroupPacketHandler.kt | 126 -- .../listener/ClientParticlePacketHandler.kt | 32 - .../ClientParticleStylePacketHandler.kt | 94 -- .../ClientRenderEntityPacketHandler.kt | 89 -- .../ClientRendererPostEffectHandler.kt | 41 - .../listener/ClientSoundInstanceHandler.kt | 11 - .../client/listener/ClientSoundLoopHandler.kt | 11 - .../server/PacketBindStarfieldFboBlockS2C.kt | 67 - .../packet/server/PacketCameraShakeS2C.kt | 210 --- .../server/PacketClearClientStateS2C.kt | 20 - .../server/PacketCooFxModelBlockTestS2C.kt | 6 - .../packet/server/PacketDataHolderS2C.kt | 109 -- .../packet/server/PacketDisplayEntityS2C.kt | 40 - .../server/PacketDisplayEntityStateS2C.kt | 44 - .../server/PacketKeyBindingCountdownS2C.kt | 26 - .../packet/server/PacketParticleBatchS2C.kt | 43 - .../PacketParticleCompositionRotateS2C.kt | 37 - .../server/PacketParticleCompositionS2C.kt | 48 - .../PacketParticleCompositionStateS2C.kt | 47 - .../server/PacketParticleEmittersS2C.kt | 73 - .../packet/server/PacketParticleGroupS2C.kt | 106 -- .../packet/server/PacketParticleS2C.kt | 40 - .../packet/server/PacketParticleStyleS2C.kt | 57 - .../packet/server/PacketRenderEntityS2C.kt | 60 - .../server/PacketRendererPostEffectS2C.kt | 92 -- .../packet/server/PacketSoundInstanceS2C.kt | 243 ---- .../packet/server/PacketSoundLoopS2C.kt | 83 -- .../server/PacketTerrainEffectGroupS2C.kt | 440 ------ .../packet/server/PacketTerrainMappingS2C.kt | 186 --- .../server/listener/ServerKeyActionHandler.kt | 17 - .../PacketPerformanceStatusControlS2C.kt | 38 - .../PacketPerformanceStatusRequestC2S.kt | 39 - .../PacketPerformanceStatusSnapshotS2C.kt | 191 --- .../PacketOpenBoundTestSelectionScreenS2C.kt | 48 - .../PacketOpenTestControllerScreenS2C.kt | 123 -- .../PacketReviewTestControllerC2S.kt | 46 - .../PacketSelectBoundTestControllerC2S.kt | 31 - .../testblock/PacketStartTestControllerC2S.kt | 31 - .../testblock/PacketStopTestControllerC2S.kt | 31 - .../PacketUpdateTestControllerC2S.kt | 80 -- .../particle/SequencedServerParticleGroup.kt | 164 --- .../network/particle/ServerParticleGroup.kt | 328 ----- .../particle/ServerParticleGroupManager.kt | 200 --- .../composition/AutoParticleComposition.kt | 18 - .../AutoSequencedParticleComposition.kt | 16 - .../particle/composition/CompositionData.kt | 51 - .../composition/ParticleComposition.kt | 1177 ----------------- .../composition/ParticleShapeComposition.kt | 156 --- .../SequencedParticleComposition.kt | 518 -------- .../SequencedParticleShapeComposition.kt | 203 --- .../manager/ParticleCompositionManager.kt | 379 ------ .../network/particle/data/DoubleRangeData.kt | 26 - .../network/particle/data/FloatRangeData.kt | 19 - .../network/particle/data/IntRangeData.kt | 24 - .../network/particle/data/RangeData.kt | 7 - .../network/particle/emitters/AutoEmitters.kt | 29 - .../particle/emitters/AutoParticleEmitters.kt | 32 - .../AutoTransformableCParticleEmitter.kt | 29 - .../emitters/CParticleEmitterSpace.kt | 34 - .../particle/emitters/ClassEmitters.kt | 411 ------ .../emitters/ClassParticleEmitters.kt | 594 --------- .../emitters/CompositionEmittersData.kt | 117 -- .../emitters/ControlableCParticleData.kt | 266 ---- .../emitters/ControlableParticleData.kt | 458 ------- .../emitters/DisplayEntityEmittersData.kt | 144 -- .../particle/emitters/ParticleDataFactory.kt | 5 - .../particle/emitters/ParticleEmitters.kt | 122 -- .../emitters/ParticleEmittersManager.kt | 398 ------ .../particle/emitters/PhysicConstant.kt | 23 - .../emitters/SimpleRandomParticleData.kt | 270 ---- .../emitters/TransformableCParticleEmitter.kt | 493 ------- .../particle/emitters/command/OrbitMode.kt | 30 - .../command/ParticleAttractionCommand.kt | 128 -- .../emitters/command/ParticleCommand.kt | 46 - .../emitters/command/ParticleCommandQueue.kt | 120 -- .../command/ParticleDistortionCommand.kt | 334 ----- .../emitters/command/ParticleDragCommand.kt | 99 -- .../command/ParticleFlowFieldCommand.kt | 119 -- .../command/ParticleGravityCommand.kt | 20 - .../emitters/command/ParticleInheritMode.kt | 6 - .../command/ParticleInheritVelocityCommand.kt | 340 ----- .../command/ParticleLifetimeMotionCommand.kt | 377 ------ .../command/ParticleLifetimeVelocityMode.kt | 7 - .../emitters/command/ParticleMotionSpace.kt | 6 - .../emitters/command/ParticleNoiseCommand.kt | 205 --- .../emitters/command/ParticleOrbitCommand.kt | 177 --- .../command/ParticleRotationForceCommand.kt | 98 -- .../ParticleToroidalCirculationCommand.kt | 226 ---- .../emitters/command/ParticleVortexCommand.kt | 161 --- .../command/curve/BezierFloatKeyframe.kt | 10 - .../command/curve/BezierKeyframeFloatCurve.kt | 250 ---- .../command/curve/ConstantFloatCurve.kt | 18 - .../emitters/command/curve/FloatCurve.kt | 22 - .../emitters/command/curve/FloatKeyframe.kt | 12 - .../command/curve/KeyframeFloatCurve.kt | 78 -- .../command/curve/LinearFloatCurve.kt | 27 - .../environment/wind/BallWindDirection.kt | 77 -- .../environment/wind/BoxWindDirection.kt | 92 -- .../environment/wind/GlobalWindDirection.kt | 68 - .../environment/wind/WindDirection.kt | 46 - .../environment/wind/WindDirections.kt | 57 - .../emitters/event/ParticleCollideEvent.kt | 25 - .../particle/emitters/event/ParticleEvent.kt | 24 - .../emitters/event/ParticleEventHandler.kt | 26 - .../event/ParticleEventHandlerManager.kt | 56 - .../emitters/event/ParticleHitEntityEvent.kt | 23 - .../emitters/event/ParticleOnGroundEvent.kt | 34 - .../emitters/event/ParticleOnLiquidEvent.kt | 24 - .../emitters/type/BoxEmittersShootType.kt | 78 -- .../emitters/type/EmittersShootType.kt | 34 - .../emitters/type/EmittersShootTypes.kt | 74 -- .../emitters/type/LineEmittersShootType.kt | 53 - .../emitters/type/MathEmittersShootType.kt | 196 --- .../emitters/type/PointEmittersShootType.kt | 48 - .../particle/style/ParticleGroupStyle.kt | 573 -------- .../particle/style/ParticleShapeStyle.kt | 222 ---- .../particle/style/ParticleStyleManager.kt | 259 ---- .../particle/style/ParticleStyleProvider.kt | 13 - .../style/SequencedParticleShapeStyle.kt | 288 ---- .../particle/style/SequencedParticleStyle.kt | 551 -------- .../particle/util/ServerParticleUtil.kt | 111 -- 157 files changed, 17943 deletions(-) delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt delete mode 100644 neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt deleted file mode 100644 index 0c548747..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class BooleanControlerBuffer() : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "boolean" - ) - ) - } - - override var loadedValue: Boolean? = false - - override fun encode(value: Boolean): ByteArray { - return byteArrayOf(if (value) 1 else 0) - } - - - override fun encode(): ByteArray { - return encode(loadedValue ?: false) - } - - override fun decode(buf: ByteArray): Boolean { - if (buf.isNotEmpty()) { - return buf[0] == 1.toByte() - } - return false - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt deleted file mode 100644 index 9fd449e4..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class CharControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "char" - ) - ) - } - override var loadedValue: Char? = ' ' - override fun encode(value: Char): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Char { - return String(buf)[0] - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt deleted file mode 100644 index 70e91e64..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class DoubleControlerBuffer() : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "double" - ) - ) - } - override var loadedValue: Double? = 0.0 - override fun encode(value: Double): ByteArray { - return value.toString().toByteArray() - } - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Double { - return String(buf).toDouble() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt deleted file mode 100644 index fa82448f..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class EmptyControlerBuffer() : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "empty" - ) - ) - } - - override var loadedValue: Unit? = null - - override fun encode(value: Unit): ByteArray { - return byteArrayOf(0) - } - - - override fun encode(): ByteArray { - return byteArrayOf() - } - - override fun decode(buf: ByteArray) { - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt deleted file mode 100644 index f538f1f4..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class FloatControlerBuffer() : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "float" - ) - ) - } - - override var loadedValue: Float? = 0.0f - override fun encode(value: Float): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Float { - return String(buf).toFloat() - - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt deleted file mode 100644 index 72236692..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - -class IntArrayControlerBuffer : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "int_array" - ) - ) - } - - override var loadedValue: IntArray? = IntArray(0) - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun encode(value: IntArray): ByteArray { - val buffer = Unpooled.buffer() - buffer.writeInt(value.size) - value.forEach { buffer.writeInt(it) } - return buffer.copy().array() - } - - override fun decode(buf: ByteArray): IntArray { - val wrap = Unpooled.wrappedBuffer(buf) - val size = wrap.readInt() - val arr = IntArray(size) - for (i in 0 until size) { - arr[i] = wrap.readInt() - } - return arr - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt deleted file mode 100644 index d2e89cf3..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class IntControlerBuffer : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "int" - ) - ) - } - - override var loadedValue: Int? = 0 - override fun encode(value: Int): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Int { - return String(buf).toInt() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt deleted file mode 100644 index fcf69566..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - -class LongArrayControlerBuffer : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "long_array" - ) - ) - } - - override var loadedValue: LongArray? = LongArray(0) - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun encode(value: LongArray): ByteArray { - val buffer = Unpooled.buffer() - buffer.writeInt(value.size) - value.forEach { - buffer.writeLong(it) - } - return buffer.copy().array() - } - - override fun decode(buf: ByteArray): LongArray { - val wrap = Unpooled.wrappedBuffer(buf) - val size = wrap.readInt() - val arr = LongArray(size) - for (i in 0 until size) { - arr[i] = wrap.readLong() - } - return arr - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt deleted file mode 100644 index c968c112..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class LongControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "long" - ) - ) - } - - override var loadedValue: Long? = 0L - override fun encode(value: Long): ByteArray { - return value.toString().toByteArray() - } - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Long { - return String(buf).toLong() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt deleted file mode 100644 index 4f40c30b..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt +++ /dev/null @@ -1,71 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - - -/** - * 如果某个style使用了嵌套 (内容使用了其他Style或者group) - * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer - * 如果你使用了自定义的ParticleControlableDataBuffer - * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe - */ -class NestedBuffersControlerBuffer : ParticleControlerDataBuffer>> { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "nested" - ) - ) - } - - override var loadedValue: Map>? = null - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun encode(value: Map>): ByteArray { - val buf = Unpooled.buffer(value.size * 8) - // 第一个存 接下来的参数个数 - // 第一个 int代表key的长度 - // 第二个 int代表value类型的字符串类的长度 - // 第三个 int代表value的长度 - buf.writeInt(value.size) - value.forEach { (key, value) -> - buf.writeInt(key.length) - buf.writeBytes(key.toByteArray()) - val id = "${value.getBufferID().value.namespace}:${value.getBufferID().value.path}" - buf.writeInt(id.length) - buf.writeBytes(id.toByteArray()) - val subBytes = value.encode()!! - buf.writeInt(subBytes.size) - buf.writeBytes(subBytes) - } - return buf.copy().array() - } - - override fun decode(buf: ByteArray): Map> { - val buf = Unpooled.copiedBuffer(buf) - val len = buf.readInt() - val res = mutableMapOf>() - for (i in 0 until len) { - val keyLen = buf.readInt() - val key = String(buf.readBytes(keyLen).copy().array()) - val typeLen = buf.readInt() - val idSplit = String(buf.readBytes(typeLen).copy().array()).split(":") - val id = ResourceLocation.fromNamespaceAndPath(idSplit[0], idSplit[1]) - val valueLen = buf.readInt() - val valueBytes = buf.readBytes(valueLen).copy().array() - val buffer = ParticleControlerDataBuffers.withIdDecode(id, valueBytes)!! - res[key] = buffer - } - return res - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt deleted file mode 100644 index b18ca163..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import net.minecraft.resources.ResourceLocation - - -interface ParticleControlerDataBuffer { - - data class Id(val value: ResourceLocation) { - companion object { - @JvmStatic - fun toID(string: String): Id { - val split = string.split(":") - if (split.size != 2) { - throw IllegalArgumentException("Invalid ID format: $string") - } - val namespace = split[0] - val id = split[1] - return Id(ResourceLocation.fromNamespaceAndPath(namespace, id)) - } - } - } - - var loadedValue: T? - fun encode(): ByteArray? - - fun encode(value: T): ByteArray - - fun decode(buf: ByteArray): T - - fun getBufferID(): Id - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt deleted file mode 100644 index acdc1c72..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt +++ /dev/null @@ -1,184 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -object ParticleControlerDataBuffers { - private val wrapperToPrimitive = mapOf, Class<*>>( - java.lang.Integer::class.java to Int::class.java, - java.lang.Double::class.java to Double::class.java, - java.lang.Long::class.java to Long::class.java, - java.lang.Float::class.java to Float::class.java, - java.lang.Boolean::class.java to Boolean::class.java, - java.lang.Character::class.java to Char::class.java, - java.lang.Byte::class.java to Byte::class.java, - java.lang.Short::class.java to Short::class.java - ) - val registerBuilder = HashMap>>() - val registerTypes = HashMap, ParticleControlerDataBuffer.Id>() - - fun boolean(value: Boolean): BooleanControlerBuffer = BooleanControlerBuffer().apply { loadedValue = (value) } - fun char(value: Char): CharControlerBuffer = CharControlerBuffer().apply { loadedValue = (value) } - fun string(value: String): StringControlerBuffer = StringControlerBuffer().apply { loadedValue = (value) } - fun int(value: Int): IntControlerBuffer = IntControlerBuffer().apply { loadedValue = (value) } - fun intArray(value: IntArray): IntArrayControlerBuffer = IntArrayControlerBuffer().apply { loadedValue = (value) } - fun double(value: Double): DoubleControlerBuffer = DoubleControlerBuffer().apply { loadedValue = (value) } - fun float(value: Float): FloatControlerBuffer = FloatControlerBuffer().apply { loadedValue = (value) } - fun long(value: Long): LongControlerBuffer = LongControlerBuffer().apply { loadedValue = (value) } - fun longArray(value: LongArray): LongArrayControlerBuffer = - LongArrayControlerBuffer().apply { loadedValue = (value) } - - fun short(value: Short): ShortControlerBuffer = ShortControlerBuffer().apply { loadedValue = (value) } - fun vec3d(value: Vec3): Vec3dControlerBuffer = Vec3dControlerBuffer().apply { loadedValue = (value) } - fun relative(value: RelativeLocation): RelativeLocationControlerBuffer = - RelativeLocationControlerBuffer().apply { loadedValue = (value) } - - fun uuid(value: UUID): UUIDControlerBuffer = UUIDControlerBuffer().apply { loadedValue = (value) } - fun empty(): EmptyControlerBuffer = EmptyControlerBuffer() - - /** - * 如果某个style使用了嵌套 (内容使用了其他Style或者group) - * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer - * 如果你使用了自定义的ParticleControlableDataBuffer - * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe - */ - fun nested(value: Map>): NestedBuffersControlerBuffer = - NestedBuffersControlerBuffer().apply { loadedValue = value } - - fun withType(value: Any, clazz: Class>): ParticleControlerDataBuffer<*> { - val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - bufferCodec.loadedValue = value - return bufferCodec - } - - fun fromBufferType(value: Any, clazz: Class<*>): ParticleControlerDataBuffer<*>? { - val targetClass = registerTypes[clazz]?.let { clazz } - ?: wrapperToPrimitive[clazz]?.takeIf { registerTypes.containsKey(it) } - ?: return null - - return registerTypes[targetClass]?.let { withId(it, value) } - } - - fun withDecode(buf: ByteArray, clazz: Class>): ParticleControlerDataBuffer<*> { - val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - bufferCodec.loadedValue = bufferCodec.decode(buf) - return bufferCodec - } - - - fun withId(id: ParticleControlerDataBuffer.Id, value: Any): ParticleControlerDataBuffer<*>? { - val clazz = registerBuilder[id] ?: return null - return withType(value, clazz) - } - - fun withIdDecode(id: ParticleControlerDataBuffer.Id, array: ByteArray): ParticleControlerDataBuffer<*>? { - val clazz = registerBuilder[id] ?: return null - return withDecode(array, clazz) - } - - fun withId(id: ResourceLocation, value: Any): ParticleControlerDataBuffer<*>? { - val identifier = ParticleControlerDataBuffer.Id(id) - val clazz = registerBuilder[identifier] ?: return null - return withType(value, clazz) - } - - fun withIdDecode(id: ResourceLocation, array: ByteArray): ParticleControlerDataBuffer<*>? { - val identifier = ParticleControlerDataBuffer.Id(id) - val clazz = registerBuilder[identifier] ?: return null - return withDecode(array, clazz) - } - - - fun register( - bufType: Class<*>, - id: ParticleControlerDataBuffer.Id, - type: Class> - ) { - registerBuilder[id] = type - registerTypes[bufType] = id - } - - fun encode(buffer: ParticleControlerDataBuffer): ByteArray { - val code = buffer.encode() ?: throw IllegalStateException("buffer encode value is null") - val decoderID = buffer::class.java.name - val byteBuf = Unpooled.buffer() - byteBuf.writeInt(decoderID.length) - byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) - byteBuf.writeBytes(code) - return byteBuf.copy().array() - } - - fun encode(value: T, buffer: ParticleControlerDataBuffer): ByteArray { - val code = buffer.encode(value) - val decoderID = buffer::class.java.name - val byteBuf = Unpooled.buffer() - byteBuf.writeInt(decoderID.length) - byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) - byteBuf.writeBytes(code) - return byteBuf.copy().array() - } - - fun decode(bytes: ByteArray): T { - // 要求目标编码器必须具有空构造函数 - val byteBuf = Unpooled.wrappedBuffer(bytes) - val len = byteBuf.readInt() - val toStringBytes = ByteArray(len) - byteBuf.readBytes(toStringBytes) - val decoderID = String(toStringBytes, Charsets.UTF_8) - val code = byteBuf.readBytes(byteBuf.readableBytes()) - val codeBytes = ByteArray(code.readableBytes()) - code.readBytes(codeBytes) - val clazz = Class.forName(decoderID) - val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - - return ins.decode(codeBytes) - } - - fun decodeToBuffer(bytes: ByteArray): ParticleControlerDataBuffer { - val buf = Unpooled.wrappedBuffer(bytes) - return decodeToBuffer(buf) - } - - fun decodeToBuffer(byteBuf: ByteBuf): ParticleControlerDataBuffer { - // 要求目标编码器必须具有空构造函数 - val len = byteBuf.readInt() - val toStringBytes = ByteArray(len) - byteBuf.readBytes(toStringBytes) - val decoderID = String(toStringBytes, Charsets.UTF_8) - val code = byteBuf.readBytes(byteBuf.readableBytes()) - val codeBytes = ByteArray(code.readableBytes()) - code.readBytes(codeBytes) - val clazz = Class.forName(decoderID) - val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - - return ins.apply { loadedValue = (ins.decode(codeBytes)) } - } - - - init { - register(Boolean::class.java, BooleanControlerBuffer.id, BooleanControlerBuffer::class.java) - register(Long::class.java, LongControlerBuffer.id, LongControlerBuffer::class.java) - register(Integer::class.java, IntControlerBuffer.id, IntControlerBuffer::class.java) - register(Double::class.java, DoubleControlerBuffer.id, DoubleControlerBuffer::class.java) - register(Float::class.java, FloatControlerBuffer.id, FloatControlerBuffer::class.java) - register(String::class.java, StringControlerBuffer.id, StringControlerBuffer::class.java) - register(IntArray::class.java, IntArrayControlerBuffer.id, IntArrayControlerBuffer::class.java) - register(LongArray::class.java, LongArrayControlerBuffer.id, LongArrayControlerBuffer::class.java) - register(UUID::class.java, UUIDControlerBuffer.id, UUIDControlerBuffer::class.java) - register(Vec3::class.java, Vec3dControlerBuffer.id, Vec3dControlerBuffer::class.java) - register( - RelativeLocation::class.java, - RelativeLocationControlerBuffer.id, - RelativeLocationControlerBuffer::class.java - ) - register(Short::class.java, ShortControlerBuffer.id, ShortControlerBuffer::class.java) - register(Unit::class.java, EmptyControlerBuffer.id, EmptyControlerBuffer::class.java) - register(Char::class.java, CharControlerBuffer.id, CharControlerBuffer::class.java) - register(Map::class.java, NestedBuffersControlerBuffer.id, NestedBuffersControlerBuffer::class.java) - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt deleted file mode 100644 index 0e4ad1f6..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - -class RelativeLocationControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "relative" - ) - ) - } - - override var loadedValue: RelativeLocation? = RelativeLocation() - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): RelativeLocation { - return RelativeLocation.fromBytes(buf) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - - override fun encode(value: RelativeLocation): ByteArray { - return value.toBytes() - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt deleted file mode 100644 index cb9f978e..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class ShortControlerBuffer() : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "short" - ) - ) - } - override var loadedValue: Short? = 0 - override fun encode(value: Short): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Short { - return String(buf).toShort() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt deleted file mode 100644 index 70e63b35..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class StringControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "string" - ) - ) - } - override var loadedValue: String? = "" - override fun encode(value: String): ByteArray { - return value.toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): String { - return String(buf) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt deleted file mode 100644 index 2df22bab..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation -import java.util.UUID - -class UUIDControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "uuid" - ) - ) - } - override var loadedValue: UUID? = UUID.randomUUID() - override fun encode(value: UUID): ByteArray { - return value.toString().toByteArray() - } - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): UUID { - return UUID.fromString(String(buf)) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt deleted file mode 100644 index 8ff7da90..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 - -class Vec3dControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "vec3d" - ) - ) - } - - override var loadedValue: Vec3? = Vec3.ZERO - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Vec3 { - val wrappedBuffer = Unpooled.wrappedBuffer(buf) - return Vec3(wrappedBuffer.readDouble(), wrappedBuffer.readDouble(), wrappedBuffer.readDouble()) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - - override fun encode(value: Vec3): ByteArray { - val buf = Unpooled.buffer() - buf.writeDouble(value.x) - buf.writeDouble(value.y) - buf.writeDouble(value.z) - return buf.copy().array() - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt deleted file mode 100644 index 4113c61f..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -import net.minecraft.client.Minecraft - -/** - * 客户端收到 [CooPacket] 时的上下文 - * - * @property packet 已解码的业务包 - * @property kind 包类型 (普通/请求/响应) - * @property correlationId 关联ID, 0 表示普通包 - * @property timeoutTicks 请求模式下的超时tick (来自发送端) - */ -class ClientContext( - val packet: CooPacket, - val kind: CooPacketKind, - val correlationId: Long, - val timeoutTicks: Int, -) { - val client: Minecraft = Minecraft.getInstance() - - /** - * 在请求模式下,向服务端回复一个响应包 - * - * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 - * - 使用本上下文携带的 [correlationId],服务端 request 注册的回调将匹配执行 - */ - fun reply(response: CooPacket) { - if (kind != CooPacketKind.REQUEST) { - throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") - } - CooClientPacketManager.replyInternal(response, correlationId) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt deleted file mode 100644 index 12f93058..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooClientPacketManager.kt +++ /dev/null @@ -1,250 +0,0 @@ -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 - -/** - * # 客户端 CooPacket 管理器 - * - * 提供: - * - [sendTo] 发送普通包到服务器 - * - [request] 向服务器发起请求, 等待指定类型响应或超时 - * - [cancelRequest] 在响应/超时前取消等待 - * - * 同时负责 envelope 包的解码、回调分发、超时扫描 (由 [tick] 推进) - * - * 默认超时为 [DEFAULT_TIMEOUT_TICKS] (60 tick = 3 秒) - */ -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() - - /** - * 发送一个普通包到服务器 - * - * @param packet 待发送的业务包 - * @return 是否发送成功 (false = 事件被取消 或 包未注册) - */ - @JvmStatic - fun sendTo(packet: CooPacket): Boolean { - return sendInternal(packet, CooPacketKind.NORMAL, 0L, 0) - } - - /** - * 向服务器发起一个请求, 等待 [R] 类型的响应 - * - * @param packet 请求包 - * @param timeoutTicks 超时 tick 数, 默认 [DEFAULT_TIMEOUT_TICKS] - * @param responseType 响应包类型 (用于运行时校验, kotlin 里可用 reified 重载) - * @param onResponse 收到响应包时的回调 (主线程) - * @return 关联ID, 可用 [cancelRequest] 取消; 若发送失败返回 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 - } - - /** - * Kotlin 友好的 reified 版本 - */ - @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 - } - - /** - * 客户端 tick 推进, 由 CooParticlesAPIClient 主循环调用 - */ - @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, - ) - ) - } - } - - /** - * 处理接收自服务端的信封 (客户端线程) - * - * 由平台 (Fabric / NeoForge) 在收到 [CooPacketEnvelopeS2C] 时调用 - */ - @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("收到未知 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 异常: ${envelope.packetId}", e) - } - - if (kind == CooPacketKind.RESPONSE) { - val pendingEntry = pending.remove(envelope.correlationId) ?: return - if (!pendingEntry.expectType.isInstance(packet)) { - CooParticlesConstants.logger.warn( - "CooPacket 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" - ) - return - } - try { - pendingEntry.callback(packet) - } catch (e: Throwable) { - CooParticlesConstants.logger.error( - "CooPacket request 回调异常 (correlationId=${envelope.correlationId})", - e - ) - } - } - } - - /** - * 内部 reply: 由 [ClientContext.reply] 调用,构造响应包发送给服务端 - */ - 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 未注册, 无法发送: ${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 编码失败: ${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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt deleted file mode 100644 index 5aa97286..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacket.kt +++ /dev/null @@ -1,56 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistryHelper -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation - -/** - * # 设计初衷 - * - 简化繁琐的数据包注册流程 采用 [cn.coostack.cooparticlesapi.annotations.CooAutoRegister] 快速注册 - * - 非必须定义包处理器 采用事件/回调方案 (基于 CooEvent) - * - 支持请求/回报的快捷方案 (基于 [CooServerPacketManager.request] / [CooClientPacketManager.request]) - * - * # 使用注意 - * - 必须提供空构造参数 (因为 codec 反射构造) - * - 所有的 CooPacket 类必须使用 [cn.coostack.cooparticlesapi.annotations.CodecField] 标明数据类型 - * - 必须使用 [cn.coostack.cooparticlesapi.annotations.CooAutoRegister] 进行自动注册标识 - * - * # 默认 timeout - * - 请求/响应模式默认超时为 60 tick (3秒) - * - * # 接收回调 - * - [onClientReceive] / [onServerReceive] 默认空实现 - * - 推荐通过 CooEvent 监听 [cn.coostack.cooparticlesapi.event.events.packet.CooPacketReceiveEvent] - * 或在子类内 override 上述方法 - */ -abstract class CooPacket { - /** - * 数据包ID 必须由子类提供,用于跨端定位包类型 - */ - abstract fun id(): ResourceLocation - - /** - * 数据包编解码器,默认基于反射 + [cn.coostack.cooparticlesapi.annotations.CodecField] 自动生成 - */ - open fun codec(): StreamCodec = - CooPacketRegistryHelper.generateClassParticleCodec(this::class.java) - - /** - * 收到自服务器的包时回调 (客户端线程) - * - * - 普通包: 直接回调 - * - 请求包: 子类应在此处理请求并通过 [ClientContext.reply] 回复 - * - 响应包: 也会调用本方法 (与 request 注册的回调并存) - */ - open fun onClientReceive(context: ClientContext) {} - - /** - * 收到自客户端的包时回调 (服务器线程) - * - * - 普通包: 直接回调 - * - 请求包: 子类应在此处理请求并通过 [ServerContext.reply] 回复 - * - 响应包: 也会调用本方法 (与 request 注册的回调并存) - */ - open fun onServerReceive(context: ServerContext) {} -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt deleted file mode 100644 index 63a52437..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt +++ /dev/null @@ -1,24 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -/** - * 数据包类型 - * - * - [NORMAL]: 普通包,无需响应 - * - [REQUEST]: 请求包,等待对端响应 - * - [RESPONSE]: 响应包,对应某个 REQUEST 的回执 - */ -enum class CooPacketKind { - NORMAL, - REQUEST, - RESPONSE; - - companion object { - private val values = entries.toTypedArray() - - fun fromId(id: Int): CooPacketKind { - return values.getOrNull(id) ?: NORMAL - } - } - - val id: Int get() = ordinal -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt deleted file mode 100644 index e03b4951..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooServerPacketManager.kt +++ /dev/null @@ -1,349 +0,0 @@ -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.network.protocol.common.custom.CustomPacketPayload -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 - -/** - * # 服务端 CooPacket 管理器 - * - * 提供: - * - [sendTo] / [sendAll] / [sendWorlds] 普通包发送 - * - [request] / [requestAll] / [requestWorlds] 请求/响应 - * - [cancelRequest] 取消等待中的请求 - * - * 默认超时 [DEFAULT_TIMEOUT_TICKS] (60 tick = 3 秒). - * - * 同时负责 envelope 解码、回调分发、超时扫描 (由 [tick] 推进) - */ -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) - } - - /** - * 广播给所有在线玩家. 每个玩家独立触发 [CooPacketSendEvent], 可分别取消 - */ - @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) - } - - // ---------------- 请求 ---------------- - - /** - * 向指定玩家发起请求, 等待 [R] 类型响应 - * - * @return 关联ID; 0 表示发送失败 - */ - @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) - - /** - * 内部使用: 把 sender 也传给回调 (用于 requestAll / requestWorlds) - */ - @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 - } - - /** - * 给所有玩家分别发起独立请求 (correlationId 各自一份, 各自计时) - * - * @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, packet, R::class.java, timeoutTicks, onResponse) - - @JvmStatic - fun cancelRequest(correlationId: Long): Boolean { - return pending.remove(correlationId) != null - } - - /** - * 服务端 tick 推进, 由 CooParticlesAPI.tickServer 调用 - */ - @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, - ) - ) - } - } - - // ---------------- 接收 ---------------- - - /** - * 由平台层 (Fabric / NeoForge) 在收到 [CooPacketEnvelopeC2S] 时调用 - */ - @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( - "收到未知 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 异常: ${envelope.packetId}", e) - } - - if (kind == CooPacketKind.RESPONSE) { - val pendingEntry = pending.remove(envelope.correlationId) ?: return - if (!pendingEntry.expectType.isInstance(packet)) { - CooParticlesConstants.logger.warn( - "CooPacket 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" - ) - return - } - try { - pendingEntry.callback(sender, packet) - } catch (e: Throwable) { - CooParticlesConstants.logger.error( - "CooPacket request 回调异常 (correlationId=${envelope.correlationId})", - e - ) - } - } - } - - // ---------------- reply ---------------- - - 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 未注册, 无法发送: ${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 编码失败: ${packet::class.java.name}", e) - return false - } - val envelope: CustomPacketPayload = 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt deleted file mode 100644 index 5d1dd997..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -import net.minecraft.server.level.ServerPlayer - -/** - * 服务端收到 [CooPacket] 时的上下文 - * - * @property sender 发送该包的玩家 - * @property packet 已解码的业务包 - * @property kind 包类型 (普通/请求/响应) - * @property correlationId 关联ID, 0 表示普通包 - * @property timeoutTicks 请求模式下的超时tick (来自发送端) - */ -class ServerContext( - val sender: ServerPlayer, - val packet: CooPacket, - val kind: CooPacketKind, - val correlationId: Long, - val timeoutTicks: Int, -) { - /** - * 在请求模式下,向客户端回复一个响应包 - * - * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 - * - 使用本上下文携带的 [correlationId],客户端 request 注册的回调将匹配执行 - */ - fun reply(response: CooPacket) { - if (kind != CooPacketKind.REQUEST) { - throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") - } - CooServerPacketManager.replyInternal(sender, response, correlationId) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt deleted file mode 100644 index ab9f4be1..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeC2S.kt +++ /dev/null @@ -1,52 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api.envelope - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation - -/** - * # CooPacket 信封 (C2S) - * - * 与 [CooPacketEnvelopeS2C] 字段一致,仅 payloadID 不同以匹配 MC 注册要求。 - */ -class CooPacketEnvelopeC2S( - val kindId: Int, - val packetId: ResourceLocation, - val correlationId: Long, - val timeoutTicks: Int, - val data: ByteArray, -) : CustomPacketPayload { - - companion object { - private val identifier = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "coo_packet_envelope_c2s") - - @JvmField - val payloadID: CustomPacketPayload.Type = - CustomPacketPayload.Type(identifier) - - @JvmField - val CODEC: StreamCodec = CustomPacketPayload.codec( - { packet, buf -> - buf.writeVarInt(packet.kindId) - buf.writeResourceLocation(packet.packetId) - buf.writeLong(packet.correlationId) - buf.writeVarInt(packet.timeoutTicks) - buf.writeByteArray(packet.data) - }, - { buf -> - CooPacketEnvelopeC2S( - kindId = buf.readVarInt(), - packetId = buf.readResourceLocation(), - correlationId = buf.readLong(), - timeoutTicks = buf.readVarInt(), - data = buf.readByteArray() - ) - } - ) - } - - override fun type(): CustomPacketPayload.Type = payloadID -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt deleted file mode 100644 index ab5c12b2..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/envelope/CooPacketEnvelopeS2C.kt +++ /dev/null @@ -1,58 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api.envelope - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation - -/** - * # CooPacket 信封 - * - * 所有[cn.coostack.cooparticlesapi.network.packet.api.CooPacket] 都通过本信封发送/接收 - * - * - kindId: 0=NORMAL, 1=REQUEST, 2=RESPONSE - * - packetId: 业务包的 ResourceLocation, 由 CooPacket.id() 提供 - * - correlationId: 0 表示普通包;非0 表示请求/响应关联ID - * - timeoutTicks: 仅请求时有效 - * - data: 业务包字段编码后的字节流 - */ -class CooPacketEnvelopeS2C( - val kindId: Int, - val packetId: ResourceLocation, - val correlationId: Long, - val timeoutTicks: Int, - val data: ByteArray, -) : CustomPacketPayload { - - companion object { - private val identifier = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "coo_packet_envelope_s2c") - - @JvmField - val payloadID: CustomPacketPayload.Type = - CustomPacketPayload.Type(identifier) - - @JvmField - val CODEC: StreamCodec = CustomPacketPayload.codec( - { packet, buf -> - buf.writeVarInt(packet.kindId) - buf.writeResourceLocation(packet.packetId) - buf.writeLong(packet.correlationId) - buf.writeVarInt(packet.timeoutTicks) - buf.writeByteArray(packet.data) - }, - { buf -> - CooPacketEnvelopeS2C( - kindId = buf.readVarInt(), - packetId = buf.readResourceLocation(), - correlationId = buf.readLong(), - timeoutTicks = buf.readVarInt(), - data = buf.readByteArray() - ) - } - ) - } - - override fun type(): CustomPacketPayload.Type = payloadID -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt deleted file mode 100644 index 72827e45..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/PacketKeyActionC2S.kt +++ /dev/null @@ -1,56 +0,0 @@ -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.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation - -/** - * 客户端按键动作触发时发送给服务器 - */ -class PacketKeyActionC2S( - val keyActions: KeyActionBatch -) : CustomPacketPayload { - companion object { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_action") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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)) - }) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt deleted file mode 100644 index 7d545a33..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt +++ /dev/null @@ -1,80 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketCameraShakeS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.utils.ClientCameraUtil -import net.minecraft.world.phys.Vec3 - -object ClientCameraShakeHandler { - fun receive( - payload: PacketCameraShakeS2C, - context: ClientContext - ) { - when (payload.operation) { - PacketCameraShakeS2C.CameraOperation.SHAKE -> handleShake(payload, context) - PacketCameraShakeS2C.CameraOperation.SET_OFFSET -> { - if (payload.instant) { - ClientCameraUtil.setOffsetNow(payload.position, payload.yawOffset, payload.pitchOffset) - } else { - ClientCameraUtil.setOffset(payload.position, payload.yawOffset, payload.pitchOffset) - } - } - - PacketCameraShakeS2C.CameraOperation.RESET_OFFSET -> { - if (payload.instant) { - ClientCameraUtil.resetOffsetNow() - } else { - ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) - } - } - - PacketCameraShakeS2C.CameraOperation.FORCE_POSITION -> { - if (payload.instant) { - ClientCameraUtil.setForcedCameraPositionNow(payload.position) - } else { - ClientCameraUtil.setForcedCameraPosition(payload.position) - } - } - - PacketCameraShakeS2C.CameraOperation.RESET_FORCE_POSITION -> { - if (payload.instant) { - ClientCameraUtil.resetForcedCameraPositionNow() - } else { - ClientCameraUtil.resetForcedCameraPosition() - } - } - - PacketCameraShakeS2C.CameraOperation.RESET_ALL -> { - if (payload.instant) { - ClientCameraUtil.resetAllNow() - } else { - ClientCameraUtil.stopShakeCamera() - ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) - ClientCameraUtil.resetForcedCameraPosition() - } - } - } - } - - private fun handleShake(payload: PacketCameraShakeS2C, context: ClientContext) { - val range = payload.range - val player = context.player() - var amplitude = payload.amplitude - var frequency = payload.frequency - if (range > 0) { - val distance = player.position().distanceTo(payload.origin) - if (distance > range) { - return - } - if (payload.attenuateByDistance) { - val attenuation = (1.0 - distance / range).coerceIn(0.0, 1.0) - amplitude *= attenuation - frequency *= attenuation - } - } - if (amplitude <= 0.0 || frequency <= 0.0) { - return - } - ClientCameraUtil.startShakeCamera(payload.tick, amplitude, frequency) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt deleted file mode 100644 index f0a997e6..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesAPIClient -import cn.coostack.cooparticlesapi.network.packet.server.PacketClearClientStateS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientClearStateHandler { - fun receive(payload: PacketClearClientStateS2C, context: ClientContext) { - context.client().execute { - CooParticlesAPIClient.clearTransientClientState() - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt deleted file mode 100644 index 6e803e15..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.data.holder.DataHolderManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketDataHolderS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientDataHolderPacketHandler { - fun receive( - payload: PacketDataHolderS2C, - context: ClientContext - ) { - val entity = context.player().level().getEntity(payload.entityId) ?: return - DataHolderManager.applyClient(entity, payload) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt deleted file mode 100644 index ec9f5d7e..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt +++ /dev/null @@ -1,50 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.display.DisplayEntity -import cn.coostack.cooparticlesapi.display.DisplayEntityManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.RegistryFriendlyByteBuf - -object ClientDisplayEntityPacketHandler { - fun receive( - payload: PacketDisplayEntityS2C, - context: ClientContext - ) { - context.client().execute { - apply(payload, context) - } - } - - private fun apply(payload: PacketDisplayEntityS2C, context: ClientContext) { - if (payload.removed) { - DisplayEntityManager.clientView.remove(payload.uuid) - return - } - val new = decodeData(payload) - new.world = context.player().level() - val old = DisplayEntityManager.clientView[payload.uuid] ?: let { - // 新建 - DisplayEntityManager.addClient(new) - return - } - // 更新 - old.update(new) - } - - private fun decodeData(payload: PacketDisplayEntityS2C): DisplayEntity { - val data = payload.data - val type = payload.type - val codec = DisplayEntityManager.registeredTypes[type]!! - val new = codec.decode( - RegistryFriendlyByteBuf( - Unpooled.wrappedBuffer(data), - Minecraft.getInstance().player!!.registryAccess() - ) - ) - return new - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt deleted file mode 100644 index 980fe036..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt +++ /dev/null @@ -1,19 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.display.DisplayEntityManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityStateS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientDisplayEntityStateHandler { - fun receive(payload: PacketDisplayEntityStateS2C, context: ClientContext) { - context.client().execute { - DisplayEntityManager.clientView[payload.uuid]?.applyRemoteState( - payload.position, - payload.yaw, - payload.pitch, - payload.roll, - payload.scale, - ) - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt deleted file mode 100644 index 9c36c138..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.key.CooKeyBindingManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketKeyBindingCountdownS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientKeyBindingCountdownHandler { - fun receive( - payload: PacketKeyBindingCountdownS2C, - context: ClientContext - ) { - CooKeyBindingManager.setCountdown(payload.key, payload.cd) - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt deleted file mode 100644 index b8d7efcd..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionS2C -import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.RegistryFriendlyByteBuf -import java.util.concurrent.ConcurrentHashMap - -object ClientParticleCompositionHandler { - private val missingCodecWarnings = ConcurrentHashMap.newKeySet() - private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() - - fun receive( - payload: PacketParticleCompositionS2C, - context: ClientContext - ) { - context.client().execute { - apply(payload, context) - } - } - - private fun apply(payload: PacketParticleCompositionS2C, context: ClientContext) { - val distanceRemove = payload.distanceRemove - // 优化新建 - var new: ParticleComposition? = null - val old = ParticleCompositionManager.clientView[payload.uuid] - if (!distanceRemove) { - // 新建 如果old为null 则直接作为新的添加在里面 - new = decodeData(payload) ?: return - if (new.controlUUID != payload.uuid && identityMismatchWarnings.add(payload.type)) { - CooParticlesConstants.logger.warn( - "Composition 网络 UUID 不一致,已采用数据包 UUID: type={}, packet={}, decoded={}", - payload.type, - payload.uuid, - new.controlUUID, - ) - } - new.controlUUID = payload.uuid - new.world = context.player().level() - } - if (old == null && !distanceRemove) { - ParticleCompositionManager.addClient(new!!) - // fix 当composition消散的时候 会抛出npe的问题 - return - } - val current = old ?: return - // new == null时 distanceRemove应该为true - if (distanceRemove) { - current.remove() - } else { - if (payload.recreate || current.canceled || !current.displayed) { - current.clear(true) - ParticleCompositionManager.addClient(new!!) - return - } - // new 不是null 且 old 不为null 更新old - current.update(new!!) - } - } - - private fun decodeData(payload: PacketParticleCompositionS2C): ParticleComposition? { - val data = payload.data - val type = payload.type - val codec = ParticleCompositionManager.registeredTypes[type] ?: run { - if (missingCodecWarnings.add(type)) { - CooParticlesConstants.logger.warn( - "收到未注册的 Composition codec: type={}, uuid={}", - type, - payload.uuid, - ) - } - return null - } - val buffer = RegistryFriendlyByteBuf( - Unpooled.wrappedBuffer(data), - Minecraft.getInstance().player!!.registryAccess() - ) - return try { - codec.decode(buffer) - } finally { - buffer.release() - } - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt deleted file mode 100644 index ac7c7f34..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt +++ /dev/null @@ -1,18 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.extend.asRelative -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionRotateS2C -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientParticleCompositionRotateHandler { - fun receive( - payload: PacketParticleCompositionRotateS2C, - context: ClientContext - ) { - context.client().execute { - val composition = ParticleCompositionManager.clientView[payload.uuid] ?: return@execute - composition.applyRemoteRotation(payload.direction?.asRelative(), payload.rollDelta) - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt deleted file mode 100644 index 70b29589..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt +++ /dev/null @@ -1,20 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionStateS2C -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientParticleCompositionStateHandler { - fun receive(payload: PacketParticleCompositionStateS2C, context: ClientContext) { - context.client().execute { - ParticleCompositionManager.clientView[payload.uuid]?.applyRemoteState( - payload.position, - payload.visibleRange, - payload.scale, - payload.displayStatus, - payload.closedInterval, - payload.current, - ) - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt deleted file mode 100644 index eac2c286..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt +++ /dev/null @@ -1,75 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleEmittersS2C -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmittersManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.RegistryFriendlyByteBuf -import java.util.concurrent.ConcurrentHashMap - -object ClientParticleEmittersPacketHandler { - private val missingCodecWarnings = ConcurrentHashMap.newKeySet() - private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() - - fun receive( - payload: PacketParticleEmittersS2C, - context: ClientContext - ) { - context.client().execute { - when (payload.type) { - PacketParticleEmittersS2C.PacketType.CREATE -> handleCreate(payload, context) - PacketParticleEmittersS2C.PacketType.CHANGE -> handleChange(payload, context) - PacketParticleEmittersS2C.PacketType.REMOVE -> handleRemove(payload) - } - } - } - - private fun handleCreate(payload: PacketParticleEmittersS2C, context: ClientContext) { - val emitters = decode(payload) ?: return - ParticleEmittersManager.createClient(emitters, context.player().level()) - } - - private fun handleChange(payload: PacketParticleEmittersS2C, context: ClientContext) { - val emitters = decode(payload) ?: return - ParticleEmittersManager.changeClient(emitters, context.player().level()) - } - - private fun handleRemove(payload: PacketParticleEmittersS2C) { - ParticleEmittersManager.removeClient(payload.emitterUUID) - } - - private fun decode(payload: PacketParticleEmittersS2C): ParticleEmitters? { - val codec = ParticleEmittersManager.getCodecFromID(payload.emitterID) ?: run { - if (missingCodecWarnings.add(payload.emitterID)) { - CooParticlesConstants.logger.warn( - "收到未注册的 Emitter codec: id={}, uuid={}", - payload.emitterID, - payload.emitterUUID, - ) - } - return null - } - val buffer = RegistryFriendlyByteBuf( - Unpooled.wrappedBuffer(payload.emitterData), - Minecraft.getInstance().level!!.registryAccess(), - ) - return try { - codec.decode(buffer).also { emitters -> - if (emitters.uuid != payload.emitterUUID && identityMismatchWarnings.add(payload.emitterID)) { - CooParticlesConstants.logger.warn( - "Emitter 网络 UUID 不一致,已采用数据包 UUID: id={}, packet={}, decoded={}", - payload.emitterID, - payload.emitterUUID, - emitters.uuid, - ) - } - emitters.uuid = payload.emitterUUID - } - } finally { - buffer.release() - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt deleted file mode 100644 index 62939ddd..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt +++ /dev/null @@ -1,126 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.particles.control.group.ClientParticleGroupManager -import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup -import cn.coostack.cooparticlesapi.particles.control.group.SequencedParticleGroup -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.Minecraft -import net.minecraft.world.phys.Vec3 -import java.util.UUID - - -object ClientParticleGroupPacketHandler { - fun receive(packet: PacketParticleGroupS2C, context: ClientContext) { - // Group UUID - val uuid = packet.uuid - // 修改类型 - val type = packet.type - when (type) { - ControlType.CREATE -> handleCreate(uuid, packet.args) - ControlType.CHANGE -> handleChange(uuid, packet.args) - ControlType.REMOVE -> handleRemove(uuid) - } - } - - - private fun handleCreate( - groupUUID: UUID, - args: Map> - ) { - val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]?.loadedValue!! as Vec3 - val world = Minecraft.getInstance().level!! - val type = args[PacketParticleGroupS2C.PacketArgsType.GROUP_TYPE.ofArgs]!!.loadedValue as String - val currentTick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue as Int - val maxTick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue as Int - val scale = args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue as Double - val groupClass = Class.forName(type) - val builder = ClientParticleGroupManager.getBuilder(groupClass as Class)!! - val group = builder.createGroup(groupUUID, args) - if (group is SequencedParticleGroup) { - handleSequencedGroupArgs(group, args) - } - group.tick = currentTick - group.maxTick = maxTick - group.scale(scale) - group.display(pos, world) - ClientParticleGroupManager.addVisibleGroup(group) - } - - private fun handleChange( - groupUUID: UUID, - args: Map> - ) { - val targetGroup = ClientParticleGroupManager.getControlGroup(groupUUID) ?: return - val argKeys = args.keys - if (PacketParticleGroupS2C.PacketArgsType.POS.ofArgs in argKeys) { - val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]!!.loadedValue!! as Vec3 - targetGroup.teleportGroupTo(pos) - } - if (PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs in argKeys) { - val to = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs]!!.loadedValue!! as Vec3 - targetGroup.rotateToPoint(RelativeLocation.of(to)) - } - if (PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs in argKeys) { - val angle = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs]!!.loadedValue as Double - targetGroup.rotateAsAxis(angle) - } - if (PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs in argKeys) { - val axis = args[PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs]!!.loadedValue!! as Vec3 - targetGroup.axis = RelativeLocation.of(axis) - } - if (PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs in argKeys) { - val tick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue!! as Int - targetGroup.tick = tick - } - if (PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs in argKeys) { - val tick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue!! as Int - targetGroup.maxTick = tick - } - if (PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs in argKeys) { - val targetMethodName = - args[PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs]!!.loadedValue!! as String - targetGroup::class.java.getDeclaredMethod(targetMethodName).apply { - isAccessible = true - }.invoke(targetGroup) - } - - if (PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs in argKeys) { - targetGroup.scale( - args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue!! as Double - ) - } - if (targetGroup is SequencedParticleGroup) { - handleSequencedGroupArgs(targetGroup, args) - } - - val builder = ClientParticleGroupManager.getBuilder(targetGroup::class.java) ?: return - builder.changeGroup(targetGroup, args) - } - - private fun handleRemove(groupUUID: UUID) { - ClientParticleGroupManager.removeVisible(groupUUID) - } - - private fun handleSequencedGroupArgs( - group: SequencedParticleGroup, - args: Map> - ) { - if (args.containsKey("toggle")) { - group.toggle(args["toggle"]!!.loadedValue as Int) - } - if (args.containsKey("addCount")) { - group.addMultiple(args["addCount"]!!.loadedValue as Int) - } - if (args.containsKey("removeCount")) { - group.removeMultiple(args["removeCount"]!!.loadedValue as Int) - } - - if (args.containsKey("toggle_status")) { - group.toggleStatus(args["toggle_status"]!!.loadedValue as LongArray) - } - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt deleted file mode 100644 index e55ab4d1..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt +++ /dev/null @@ -1,32 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientParticlePacketHandler { - fun receive(packet: PacketParticleBatchS2C, context: ClientContext) { - val world = context.player().level() - packet.positions.forEach { position -> - world.addParticle( - packet.type, - true, - position.x, - position.y, - position.z, - packet.velocity.x, - packet.velocity.y, - packet.velocity.z, - ) - } - } - - fun receive(packet: PacketParticleS2C, context: ClientContext) { - val player = context.player() - val world = player.level() - world.addParticle( - packet.type, true, packet.pos.x, packet.pos.y, packet.pos.z, - packet.velocity.x, packet.velocity.y, packet.velocity.z, - ) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt deleted file mode 100644 index 66bacfc1..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt +++ /dev/null @@ -1,94 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C -import cn.coostack.cooparticlesapi.network.particle.style.ParticleGroupStyle -import cn.coostack.cooparticlesapi.network.particle.style.ParticleStyleManager -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.Minecraft -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -object ClientParticleStylePacketHandler { - fun receive(packet: PacketParticleStyleS2C, context: ClientContext) { - // Group UUID - val uuid = packet.uuid - // 修改类型 - val type = packet.type - when (type) { - ControlType.CREATE -> handleCreate(uuid, packet.args) - ControlType.CHANGE -> handleChange(uuid, packet.args) - ControlType.REMOVE -> handleRemove(uuid) - } - } - - private fun handleRemove(uuid: UUID) { - ParticleStyleManager.clientViewStyles[uuid]?.remove() - } - - /** 处理客户端同步修改 */ - private fun handleChange( - uuid: UUID, - args: Map> - ) { - // 确保他在 clientView - val style = ParticleStyleManager.clientViewStyles[uuid] ?: return - if (args.containsKey("pos")) { - val pos = args["pos"]!!.loadedValue as Vec3 - style.teleportTo(pos) - } - if (args.containsKey("teleport")) { - val pos = args["teleport"]!!.loadedValue as Vec3 - style.teleportTo(pos) - } - if (args.containsKey("rotate_to") && args.containsKey("rotate_angle")) { - style.rotateToWithAngle( - args["rotate_to"]!!.loadedValue as RelativeLocation, - args["rotate_angle"]!!.loadedValue as Double - ) - } else { - if (args.containsKey("rotate_angle")) { - style.rotateAsAxis(args["rotate_angle"]!!.loadedValue as Double) - } - if (args.containsKey("rotate_to")) { - style.rotateToPoint(args["rotate_to"]!!.loadedValue as RelativeLocation) - } - } - if (args.containsKey("axis")) { - style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) - } - if (args.containsKey("scale")) { - style.scale = args["scale"]!!.loadedValue as Double - } - if (args.containsKey("lastUpdatedGameTime")) { - style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long - } - if (args.containsKey("displayedTime")) { - style.displayedTime = args["displayedTime"]!!.loadedValue as Long - } - style.readPacketArgs(args) - } - - /** 处理客户端同步创建 */ - private fun handleCreate( - uuid: UUID, - args: Map> - ) { - val clazz = args["style_type"]!!.loadedValue as String - val builderType = Class.forName(clazz) - val builder = ParticleStyleManager.getBuilder(builderType as Class) ?: return - val style = builder.createStyle(uuid, args) - val pos = args["pos"]!!.loadedValue as Vec3 - style.rotate = args["rotate"]!!.loadedValue as Double - style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) - style.scale = args["scale"]!!.loadedValue as Double - style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long - style.displayedTime = args["displayedTime"]!!.loadedValue as Long - val world = Minecraft.getInstance().level - style.readPacketArgs(args) - ParticleStyleManager.spawnStyle(world!!, pos, style) - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt deleted file mode 100644 index acacd8c5..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.coofx.server.CooFxSceneRenderEntity -import cn.coostack.cooparticlesapi.network.packet.server.PacketRenderEntityS2C -import cn.coostack.cooparticlesapi.coofx.client.CooFxSceneClientRegistry -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.renderer.client.ClientRenderEntityManager -import cn.coostack.cooparticlesapi.renderer.runtime.ClientRenderEntityRegistry -import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityInstance -import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityRenderer -import cn.coostack.cooparticlesapi.renderer.RenderEntity -import io.netty.buffer.Unpooled -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.resources.ResourceLocation - -/** 把服务端 RenderEntity 同步包应用到客户端实体运行时。 */ -object ClientRenderEntityPacketHandler { - /** - * 解码同步数据,并在客户端线程执行创建、更新或移除操作。 - * - * @param packet 服务端发送的 RenderEntity 操作和序列化数据 - * @param context 提供客户端线程与当前客户端实例的网络上下文 - */ - fun receive( - packet: PacketRenderEntityS2C, - context: ClientContext - ) { - val method = packet.method - val data = packet.entityData - val id = packet.id - val buf = FriendlyByteBuf(Unpooled.wrappedBuffer(data)) - val type = ClientRenderEntityRegistry.get(id) - if (type == null) { - CooParticlesConstants.logger.error( - "[CooFX-CREATE] 客户端 RenderEntity 类型未注册:method=$method, packetUuid=${packet.uuid}, typeId=$id", - ) - return - } - val entity = type.codec.decode(buf) - context.client().execute { - entity.world = context.client().level - when (method) { - PacketRenderEntityS2C.Method.CREATE -> { - val renderer = resolveRenderer(entity, id) - val instance = RenderEntityInstance(entity, renderer) - ClientRenderEntityManager.add(instance) - (entity as? CooFxSceneRenderEntity)?.let { created -> - CooFxSceneClientRegistry.onCreated(created) - } - } - - PacketRenderEntityS2C.Method.TOGGLE -> { - val instance = ClientRenderEntityManager.getFrom(packet.uuid) - instance?.updateFrom(entity) - (instance?.entity as? CooFxSceneRenderEntity)?.let { updated -> - CooFxSceneClientRegistry.onUpdated(updated) - } - } - - PacketRenderEntityS2C.Method.REMOVE -> { - ClientRenderEntityManager.getFrom(packet.uuid)?.markRemoved() - CooFxSceneClientRegistry.onRemoved(packet.uuid) - } - } - } - } - - /** - * 解析实体使用的 renderer。旧式实体自带 renderer 时保留实例行为,V2 类型从注册表取共享实例。 - * - * @param entity 已解码的客户端实体 - * @param id 实体类型注册 id - * @return 可渲染当前实体的 renderer - * @throws IllegalStateException 类型没有注册 renderer 时抛出 - */ - @Suppress("UNCHECKED_CAST") - private fun resolveRenderer( - entity: RenderEntity, - id: ResourceLocation - ): RenderEntityRenderer { - if (entity is RenderEntityRenderer<*>) { - return entity as RenderEntityRenderer - } - val renderer = ClientRenderEntityRegistry.resolveRenderer(id) - ?: throw IllegalStateException("RenderEntity renderer not registered: $id") - return renderer as RenderEntityRenderer - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt deleted file mode 100644 index b447f9e3..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt +++ /dev/null @@ -1,41 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.server.PacketRendererPostEffectS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.renderer.post.CooPostEffects -import cn.coostack.cooparticlesapi.renderer.post.PostEffectRuntimeRegistry - -object ClientRendererPostEffectHandler { - fun receive(packet: PacketRendererPostEffectS2C, context: ClientContext) { - context.client().execute { - apply(packet) - } - } - - internal fun apply(packet: PacketRendererPostEffectS2C) { - when (packet.operation) { - PacketRendererPostEffectS2C.Operation.CREATE, - PacketRendererPostEffectS2C.Operation.UPDATE -> applyState(packet) - PacketRendererPostEffectS2C.Operation.REMOVE -> CooPostEffects.client.remove(packet.instanceId) - } - } - - private fun applyState(packet: PacketRendererPostEffectS2C) { - val state = packet.state ?: return - if (!PostEffectRuntimeRegistry.containsType(state.effectType)) { - CooParticlesConstants.logger.warn( - "Skipping synced post effect id={} because type={} is not registered on the client", - state.instanceId, - state.effectType - ) - return - } - val instance = state.instantiate(serverSynced = true) ?: return - when (packet.operation) { - PacketRendererPostEffectS2C.Operation.CREATE -> CooPostEffects.client.add(instance) - PacketRendererPostEffectS2C.Operation.UPDATE -> CooPostEffects.client.update(instance) - PacketRendererPostEffectS2C.Operation.REMOVE -> Unit - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt deleted file mode 100644 index 34d09f43..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundInstanceS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.supports.sound.ClientSoundManager - -object ClientSoundInstanceHandler { - fun receive(payload: PacketSoundInstanceS2C, context: ClientContext) { - ClientSoundManager.handle(payload) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt deleted file mode 100644 index 613486cc..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundLoopS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.supports.sound.ClientSoundLoopManager - -object ClientSoundLoopHandler { - fun receive(payload: PacketSoundLoopS2C, context: ClientContext) { - ClientSoundLoopManager.handle(payload) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt deleted file mode 100644 index 32a3b9b2..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt +++ /dev/null @@ -1,67 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.renderer.pipeline.CooBlockPipelines -import cn.coostack.cooparticlesapi.test.options.renderer.pipeline.RenderPipelineExamples -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.level.block.Blocks -import net.minecraft.world.level.block.state.BlockState -import java.util.UUID - -/** - * 把方块测试选中的方块类型同步为客户端星空 FBO 临时绑定。 - * - * BlockState 只用于通过注册表同步方块类型。每次测试都有独立的 [bindingId],结束时只撤销 - * 自己的规则,之前的永久绑定或其他测试绑定会继续生效。 - */ -@CooAutoRegister -class PacketBindStarfieldFboBlockS2C() : CooPacket() { - /** 用于同步目标方块类型的状态载体。 */ - @CodecField - var state: BlockState = Blocks.AIR.defaultBlockState() - - /** 当前临时绑定的所有者 ID。 */ - @CodecField - var bindingId: UUID = UUID(0L, 0L) - - /** `true` 添加临时绑定,`false` 撤销该所有者的绑定。 */ - @CodecField - var enabled: Boolean = true - - /** - * @param state 测试位置的方块状态,用于传递方块类型 - * @param bindingId 当前测试的临时绑定 ID - * @param enabled 是否启用星空 FBO 绑定 - */ - constructor(state: BlockState, bindingId: UUID, enabled: Boolean) : this() { - this.state = state - this.bindingId = bindingId - this.enabled = enabled - } - - /** @return 星空 FBO 方块类型绑定包的稳定资源 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** - * @param context 当前客户端网络上下文 - */ - override fun onClientReceive(context: ClientContext) { - context.client.execute { - if (enabled) { - CooBlockPipelines.bindScoped(bindingId, state.block, RenderPipelineExamples.STARFIELD_BLOCK) - } else { - CooBlockPipelines.unbindScoped(bindingId) - } - } - } - - companion object { - private const val PACKET_ID = "bind_starfield_fbo_block_s2c" - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt deleted file mode 100644 index 4e283069..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCameraShakeS2C.kt +++ /dev/null @@ -1,210 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -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 -) : CustomPacketPayload { - 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 = CustomPacketPayload.Type(identifierID) - - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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.values().firstOrNull { it.id == id } ?: CameraOperation.SHAKE - } - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt deleted file mode 100644 index 001dae36..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketClearClientStateS2C.kt +++ /dev/null @@ -1,20 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation - -object PacketClearClientStateS2C : CustomPacketPayload { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "clear_client_state") - - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ _, _ -> }, { _ -> PacketClearClientStateS2C }) - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt deleted file mode 100644 index db295522..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -/** - * 旧版 BlockTest 的一次性 CooFX S2C 包已由服务端权威 `CooFxSceneManager` 和 RenderEntity - * CREATE/TOGGLE/REMOVE 协议替代。本文件保留迁移说明,避免再次引入测试专用网络状态。 - */ diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt deleted file mode 100644 index c8d32fe5..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDataHolderS2C.kt +++ /dev/null @@ -1,109 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.data.holder.DataHolder -import cn.coostack.cooparticlesapi.data.holder.DataHolderKey -import cn.coostack.cooparticlesapi.data.holder.DataHolderManager -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.entity.Entity - -class PacketDataHolderS2C( - val entityId: Int, - val cacheAllToggle: Boolean, - val fullSync: Boolean, - val entries: List -) : CustomPacketPayload { - 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 = CustomPacketPayload.Type(id) - val CODEC: StreamCodec = - StreamCodec.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: Entity, 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: Entity): ByteArray { - val codec = DataHolderManager.getCodecFromID(type) - ?: throw IllegalStateException("DataHolder codec not registered for type: $type") - val buf = RegistryFriendlyByteBuf( - Unpooled.buffer(), - entity.level().registryAccess() - ) - @Suppress("UNCHECKED_CAST") - (codec as StreamCodec).encode(buf, value) - val data = ByteArray(buf.readableBytes()) - buf.readBytes(data) - return data - } - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } - - fun decodeData(): Map, Any> { - val entity = Minecraft.getInstance().level?.getEntity(entityId) - ?: return emptyMap() - return entries.associate { entry -> - val keyType = runCatching { - Class.forName(entry.type) - }.getOrDefault(Any::class.java) - val key = DataHolderKey.ofRaw(keyType, ResourceLocation.parse(entry.key)) - key to decodeValue(entry.type, entry.data, entity) - } - } - - private fun decodeValue(type: String, data: ByteArray, entity: Entity): Any { - val codec = DataHolderManager.getCodecFromID(type) - ?: throw IllegalStateException("DataHolder codec not registered for type: $type") - val buf = RegistryFriendlyByteBuf( - Unpooled.wrappedBuffer(data), - Minecraft.getInstance().connection?.registryAccess() - ?: entity.level().registryAccess() - ) - @Suppress("UNCHECKED_CAST") - return (codec as StreamCodec).decode(buf) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt deleted file mode 100644 index c3ab0b3a..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityS2C.kt +++ /dev/null @@ -1,40 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import java.util.UUID - - -class PacketDisplayEntityS2C( - val uuid: UUID, - val type: String, - val data: ByteArray, - val removed: Boolean = false -) : CustomPacketPayload { - companion object { - private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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) - }) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt deleted file mode 100644 index bfd7d448..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketDisplayEntityStateS2C.kt +++ /dev/null @@ -1,44 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -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, -) : CustomPacketPayload { - companion object { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "display_entity_state") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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(), - ) - }) - } - - override fun type(): CustomPacketPayload.Type = payloadID -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt deleted file mode 100644 index d5a6442d..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketKeyBindingCountdownS2C.kt +++ /dev/null @@ -1,26 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation - -class PacketKeyBindingCountdownS2C(val key: ResourceLocation, val cd: Int) : CustomPacketPayload { - companion object { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_binding_countdown") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ packet, buf -> - buf.writeResourceLocation(packet.key) - buf.writeInt(packet.cd) - }, { buf -> - PacketKeyBindingCountdownS2C(buf.readResourceLocation(), buf.readInt()) - }) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt deleted file mode 100644 index aa80e256..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleBatchS2C.kt +++ /dev/null @@ -1,43 +0,0 @@ -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.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 - -class PacketParticleBatchS2C( - val type: ParticleOptions, - val positions: List, - val velocity: Vec3, -) : CustomPacketPayload { - 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 = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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) - }) - } - - override fun type(): CustomPacketPayload.Type = payloadID -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt deleted file mode 100644 index 87c89bef..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionRotateS2C.kt +++ /dev/null @@ -1,37 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -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 -) : CustomPacketPayload { - companion object { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_rotate") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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) - }) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt deleted file mode 100644 index 9359aa65..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionS2C.kt +++ /dev/null @@ -1,48 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import java.util.UUID - - -class PacketParticleCompositionS2C(val uuid: UUID, val type: String, val data: ByteArray) : CustomPacketPayload { - - /** - * 是否是因为距离过长而移除 - */ - var distanceRemove = false - var recreate = false - - companion object { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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 - } - }) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt deleted file mode 100644 index 6ef54a62..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleCompositionStateS2C.kt +++ /dev/null @@ -1,47 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -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, -) : CustomPacketPayload { - companion object { - private val identifierID = - ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_composition_state") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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(), - ) - }) - } - - override fun type(): CustomPacketPayload.Type = payloadID -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt deleted file mode 100644 index c70f2a1f..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleEmittersS2C.kt +++ /dev/null @@ -1,73 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import java.util.UUID - -class PacketParticleEmittersS2C( - val emitterID: String, - val emitterUUID: UUID, - val emitterData: ByteArray, - val type: PacketType -) : - CustomPacketPayload { - /** - * 发射器同步包的生命周期操作。 - * - * [CREATE] 用于首次进入玩家可见范围,客户端必须创建并启动新实例; - * [CHANGE] 只更新已经存在的客户端实例,不得重新执行启动逻辑; - * [REMOVE] 按 UUID 立即清理客户端实例,不携带完整发射器状态。 - * 数字 ID 是网络协议的一部分,其中 `0` 和 `1` 保留旧协议的创建与移除编号。 - */ - 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 = CustomPacketPayload.Type(id) - - val CODEC = - StreamCodec.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) - ) - }) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt deleted file mode 100644 index 153a4080..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleGroupS2C.kt +++ /dev/null @@ -1,106 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.particles.control.ControlType -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import java.util.UUID - -/** - * 三个参数 - * @param uuid 更改的粒子组的UUID - * @param type 三种类型对应三种不同的包参数 - * 1. REMOVE args什么都不写 - * 2. CREATE args输入 - * 1. pos: Vec3d - * 2. groupType: Class - * 3. currentTick: Int - * 4. maxTick: Int - * 5. ByteBuf 用于ControlableParticleGroup 构建的输入参数(不包括自身的UUID) - * 6. scale 缩放大小 - * 3. CHANGE args输入支持 - * 1. pos: Vec3d - * 2. rotateTo: Vec3d - * 3. rotateAxis: Double / Float - * 4. axis: Vec3d - * 5. currentTick: Int - * 6. maxTick: Int - * 7. invoke: 无参方法名 - * 8. scale 缩放大小 - */ -class PacketParticleGroupS2C( - val uuid: UUID, - val type: ControlType, - val args: Map> -) : CustomPacketPayload { - 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" -> return POS - "current_tick" -> return CURRENT_TICK - "max_tick" -> return MAX_TICK - "rotate_to" -> return ROTATE_TO - "rotate_axis" -> return ROTATE_AXIS - "invoke" -> return INVOKE - "scale" -> return SCALE - "groupType" -> return GROUP_TYPE - else -> INVOKE - } - } - } - } - - companion object { - private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_group") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ packet, buf -> - buf.writeUUID(packet.uuid) - buf.writeInt(packet.type.id) - packet.args.forEach { (t, u) -> - val encode = 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 = 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 = ParticleControlerDataBuffers.decodeToBuffer(value) - args[key] = decode - } - return@codec PacketParticleGroupS2C( - uuid, type, args - ) - } - ) - - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt deleted file mode 100644 index e0c78a7d..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleS2C.kt +++ /dev/null @@ -1,40 +0,0 @@ -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.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 - -/** - * - */ -class PacketParticleS2C( - val type: ParticleOptions, - val pos: Vec3, - val velocity: Vec3, -) : CustomPacketPayload { - companion object { - private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ packet, buf -> - buf.writeVec3(packet.pos) - buf.writeVec3(packet.velocity) - ParticleTypes.STREAM_CODEC.encode(buf, packet.type) - }, { buf -> - val pos = buf.readVec3() - val velocity = buf.readVec3() - val type = ParticleTypes.STREAM_CODEC.decode(buf) - return@codec PacketParticleS2C(type, pos, velocity) - }) - - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt deleted file mode 100644 index 41faef4c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketParticleStyleS2C.kt +++ /dev/null @@ -1,57 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.particles.control.ControlType -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import java.util.UUID - -class PacketParticleStyleS2C( - val uuid: UUID, - val type: ControlType, - val args: Map> -) : CustomPacketPayload { - companion object { - private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_style") - val payloadID = CustomPacketPayload.Type(identifierID) - val CODEC: StreamCodec = - CustomPacketPayload.codec({ packet, buf -> - buf.writeUUID(packet.uuid) - buf.writeInt(packet.type.id) - buf.writeInt(packet.args.size) - packet.args.forEach { (t, u) -> - val encode = 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 = ControlType.getTypeById(id) - val argsCount = buf.readInt() - repeat(argsCount) { - val len = buf.readInt() - val key = buf.readUtf() - val buf = buf.readBytes(len) - val decode = ParticleControlerDataBuffers.decodeToBuffer(buf) - args[key] = decode - } - return@codec PacketParticleStyleS2C( - uuid, type, args - ) - } - ) - - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt deleted file mode 100644 index ecf56ca6..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRenderEntityS2C.kt +++ /dev/null @@ -1,60 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation -import java.util.UUID - -class PacketRenderEntityS2C(var uuid: UUID, var entityData: ByteArray, var id: ResourceLocation, var method: Method) : - CustomPacketPayload { - 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 = CustomPacketPayload.Type(identifierID) - - @JvmStatic - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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) - val packet = PacketRenderEntityS2C(uuid, bytes, id, Method.idOf(method)) - return@codec packet - } - ) - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt deleted file mode 100644 index 064a32ca..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketRendererPostEffectS2C.kt +++ /dev/null @@ -1,92 +0,0 @@ -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 net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.resources.ResourceLocation - -class PacketRendererPostEffectS2C private constructor( - internal val operation: Operation, - internal val state: SyncedPostEffectState?, - internal val instanceId: String -) : CustomPacketPayload { - 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 = CustomPacketPayload.Type(identifierID) - - @JvmStatic - val CODEC: StreamCodec = - CustomPacketPayload.codec({ 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: FriendlyByteBuf) { - 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: FriendlyByteBuf): 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() - ) - } - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt deleted file mode 100644 index ab31292f..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundInstanceS2C.kt +++ /dev/null @@ -1,243 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -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 -) : CustomPacketPayload { - 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 = CustomPacketPayload.Type(identifierID) - - private val emptySound = ResourceLocation.withDefaultNamespace("empty") - - val CODEC: StreamCodec = CustomPacketPayload.codec( - { 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: FriendlyByteBuf, values: Set) { - buf.writeVarInt(values.size) - values.forEach(buf::writeResourceLocation) - } - - private fun readResourceLocationSet(buf: FriendlyByteBuf): Set { - val result = LinkedHashSet() - repeat(buf.readVarInt()) { - result.add(buf.readResourceLocation()) - } - return result - } - - private fun writeSoundSourceSet(buf: FriendlyByteBuf, values: Set) { - buf.writeVarInt(values.size) - values.forEach(buf::writeEnum) - } - - private fun readSoundSourceSet(buf: FriendlyByteBuf): Set { - val result = LinkedHashSet() - repeat(buf.readVarInt()) { - result.add(buf.readEnum(SoundSource::class.java)) - } - return result - } - - private fun writeStringSet(buf: FriendlyByteBuf, values: Set) { - buf.writeVarInt(values.size) - values.forEach(buf::writeUtf) - } - - private fun readStringSet(buf: FriendlyByteBuf): Set { - val result = LinkedHashSet() - repeat(buf.readVarInt()) { - result.add(buf.readUtf()) - } - return result - } - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt deleted file mode 100644 index 6b1db22e..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketSoundLoopS2C.kt +++ /dev/null @@ -1,83 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -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 -) : CustomPacketPayload { - companion object { - private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_loop") - val payloadID = CustomPacketPayload.Type(identifierID) - - val CODEC: StreamCodec = CustomPacketPayload.codec( - { 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 - ) - } - } - - override fun type(): CustomPacketPayload.Type { - return payloadID - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt deleted file mode 100644 index 545139bb..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainEffectGroupS2C.kt +++ /dev/null @@ -1,440 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -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.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation - -/** - * 从服务端向客户端同步一个地形效果组的替换或增量更新。 - * - * 协议支持完整替换、追加位置、移除位置、替换 uniform 和删除整组。位置使用相对首坐标的 ZigZag - * 差值编码,生效时间使用相对首个绝对 tick 的差值编码,避免大批连续方块重复传输完整坐标和时间。 - * 包实例只由 companion object 中的工厂创建,并由 - * [cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectManager] 发送。 - */ -@CooAutoRegister -class PacketTerrainEffectGroupS2C() : CooPacket() { - /** 当前包的协议操作类型。 */ - var operation: Int = REPLACE - - /** 目标效果组所属的维度 ID。 */ - var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") - - /** 目标效果组 ID。 */ - var groupId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") - - /** 完整替换操作引用的方块 Pipeline ID。 */ - var pipelineId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") - - /** 完整替换操作中组开始的绝对游戏 tick。 */ - var startedAt: Long = 0L - - /** 完整替换操作中组到期的绝对游戏 tick;`null` 表示持久组。 */ - var expiresAt: Long? = null - - /** 完整替换操作中用于重叠组排序的服务端序号。 */ - var sequence: Long = 0L - - /** 用于丢弃重复或乱序更新的单调协议版本号。 */ - var revision: Long = 0L - - /** 完整替换和 options 更新中的有符号绘制优先级。 */ - var priority: Int = 0 - - /** 完整替换和 options 更新中的合成方式。 */ - var composition: CooTerrainEffectComposition = CooTerrainEffectComposition.REPLACE - - /** - * 操作涉及的位置数据。 - * - * 完整替换和追加操作中,键为方块坐标,值为绝对生效 tick;移除位置操作只使用键。 - */ - var positions: Map = emptyMap() - - /** 键为 shader uniform 名称,值为完整替换或 uniform 更新携带的数据。 */ - var uniforms: Map = emptyMap() - - /** @return 地形效果组服务端到客户端协议的固定资源 ID。 */ - override fun id(): ResourceLocation = ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, - PACKET_ID - ) - - /** @return 编解码本协议全部操作类型的 StreamCodec。 */ - override fun codec(): StreamCodec = CODEC - - /** - * 在客户端主线程把本包更新应用到 [CooTerrainEffectRegistry]。 - * - * @param context 当前客户端网络处理上下文 - */ - override fun onClientReceive(context: 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 - ) - } - } - } - - /** 创建各协议操作并实现紧凑的坐标、时间和 uniform 编解码。 */ - 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 - - private val CODEC: StreamCodec = - StreamCodec.of(::encode, ::decode) - - /** - * 创建完整替换效果组的协议包。 - * - * @param snapshot 要同步的完整服务端效果组快照 - * @return 携带 Pipeline、时间、位置和 uniform 的替换包 - */ - 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 - } - } - - /** - * 创建只追加位置和绝对生效时间的协议包。 - * - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @param positions 键为新增方块坐标,值为绝对生效 tick - * @return 位置追加包 - */ - 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 - } - } - - /** - * 创建删除整个效果组的协议包。 - * - * @param dimension 组所属维度 ID - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @return 整组删除包 - */ - 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 - } - } - - /** - * 创建只删除组内指定位置的协议包。 - * - * @param dimension 组所属维度 ID - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @param positions 要删除的方块坐标 - * @return 位置删除包 - */ - 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 } - } - } - - /** - * 创建替换整组 uniform 集合的协议包。 - * - * @param dimension 组所属维度 ID - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @param uniforms 键为 shader uniform 名称,值为新的完整 uniform 数据 - * @return uniform 更新包 - */ - 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: FriendlyByteBuf, 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: FriendlyByteBuf): 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 - } - - /** - * 以首项为坐标和时间基准写入一组位置。 - * - * @param buffer 目标网络缓冲 - * @param positions 键为方块坐标,值为该位置的绝对生效 tick - */ - private fun writeTimedPositions(buffer: FriendlyByteBuf, 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)) - } - } - - /** - * 读取以首项为基准编码的位置和绝对生效时间。 - * - * @param buffer 源网络缓冲 - * @return 键为方块坐标、值为绝对生效 tick 的映射 - */ - private fun readTimedPositions(buffer: FriendlyByteBuf): 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: FriendlyByteBuf, 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)) - } - } - - /** @return 从相对坐标差值还原出的方块位置集合。 */ - private fun readPositions(buffer: FriendlyByteBuf): 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()) - ) - ) - } - } - } - - /** - * 写入带类型标记的 uniform 映射。 - * - * @param buffer 目标网络缓冲 - * @param uniforms 键为 uniform 名称,值为浮点数、整数或向量数据 - */ - private fun writeUniforms(buffer: FriendlyByteBuf, uniforms: Map) { - buffer.writeVarInt(uniforms.size) - uniforms.forEach { (name, value) -> - buffer.writeUtf(name) - CooUniformValue.STREAM_CODEC.encode(buffer, value) - } - } - - /** - * 读取带类型标记的 uniform 映射。 - * - * @param buffer 源网络缓冲 - * @return 键为 uniform 名称、值为对应类型数据的映射 - */ - private fun readUniforms(buffer: FriendlyByteBuf): 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt deleted file mode 100644 index 3fe7248c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketTerrainMappingS2C.kt +++ /dev/null @@ -1,186 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -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.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation - -/** - * 程序化 terrain mapping 的服务端到客户端同步包。 - * - * 包只携带区域参数和实例参数,不携带 BlockPos、非空气方块或高度图结果。 - */ -@CooAutoRegister -class PacketTerrainMappingS2C() : CooPacket() { - 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(): StreamCodec = CODEC - - override fun onClientReceive(context: 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 - private val CODEC: StreamCodec = - StreamCodec.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: FriendlyByteBuf, 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: FriendlyByteBuf): 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: FriendlyByteBuf, uniforms: Map) { - buffer.writeVarInt(uniforms.size) - uniforms.forEach { (name, value) -> - buffer.writeUtf(name) - CooUniformValue.STREAM_CODEC.encode(buffer, value) - } - } - - private fun readUniforms(buffer: FriendlyByteBuf): Map { - val count = buffer.readVarInt() - val result = LinkedHashMap(count) - repeat(count) { - result[buffer.readUtf()] = CooUniformValue.STREAM_CODEC.decode(buffer) - } - return result - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt deleted file mode 100644 index d010466c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt +++ /dev/null @@ -1,17 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server.listener - -import cn.coostack.cooparticlesapi.event.CooEventBus -import cn.coostack.cooparticlesapi.event.events.key.KeyActionEvent -import cn.coostack.cooparticlesapi.network.packet.client.PacketKeyActionC2S -import cn.coostack.cooparticlesapi.platform.network.ServerContext - -object ServerKeyActionHandler { - fun receive(packet: PacketKeyActionC2S, context: ServerContext) { - val player = context.player() - context.server().execute { - CooEventBus.call( - KeyActionEvent(player, packet.keyActions, true) - ) - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt deleted file mode 100644 index bc7b67d0..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt +++ /dev/null @@ -1,38 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.status - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.performance.PerformanceStatusClientBridge -import cn.coostack.cooparticlesapi.performance.PerformanceStatusControlAction -import net.minecraft.resources.ResourceLocation - -/** 服务端命令向目标玩家客户端发送的 Status 会话控制包。 */ -@CooAutoRegister -class PacketPerformanceStatusControlS2C() : CooPacket() { - /** 网络动作名称。 */ - @CodecField var action: String = "" - - /** 使用类型安全动作创建控制包。 */ - constructor(action: PerformanceStatusControlAction) : this() { - this.action = action.name - } - - /** 返回该业务包的稳定协议 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** 经纯 common bridge 把动作交给客户端初始化时安装的控制器。 */ - override fun onClientReceive(context: ClientContext) { - PerformanceStatusControlAction.fromNetworkName(action)?.let(PerformanceStatusClientBridge::handle) - } - - /** 协议注册常量。 */ - companion object { - /** 业务包稳定注册 ID,属于网络协议的一部分。 */ - private const val PACKET_ID = "performance_status_control_s2c" - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt deleted file mode 100644 index feb102c0..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt +++ /dev/null @@ -1,39 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.status - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.CooPacketKind -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerRequestGate -import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshotFactory -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.resources.ResourceLocation - -/** 客户端在 Status 活跃期间按配置间隔发送的无参数服务端快照请求。 */ -@CooAutoRegister -class PacketPerformanceStatusRequestC2S : CooPacket() { - /** 返回该业务包的稳定协议 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** 验证请求类型、命令权限和服务端限流后构造固定大小快照。 */ - override fun onServerReceive(context: ServerContext) { - if (context.kind != CooPacketKind.REQUEST) return - val refreshInterval = CooParticlesServices.API_CONFIG_MANAGER.getConfig() - .statusServerRefreshIntervalTicks - if (!PerformanceStatusServerRequestGate.tryAcquire(context.sender, refreshInterval)) return - context.reply( - PacketPerformanceStatusSnapshotS2C.from( - PerformanceStatusServerSnapshotFactory.create(context.sender.server) - ) - ) - } - - /** 协议注册常量。 */ - companion object { - /** 业务包稳定注册 ID,属于网络协议的一部分。 */ - private const val PACKET_ID = "performance_status_request_c2s" - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt deleted file mode 100644 index 7094ae10..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt +++ /dev/null @@ -1,191 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.status - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkTotals -import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshot -import cn.coostack.cooparticlesapi.performance.PerformanceStatusVanillaPacketTotals -import net.minecraft.resources.ResourceLocation - -/** 服务端返回给单个查看者的固定字段 Status 快照。 */ -@CooAutoRegister -class PacketPerformanceStatusSnapshotS2C : CooPacket() { - /** 服务端平均 MSPT。 */ - @CodecField var averageMspt: Double = 0.0 - - /** 服务端 Barrage 数。 */ - @CodecField var barrages: Int = 0 - - /** 服务端生成快照的 epoch 毫秒。 */ - @CodecField var capturedAtEpochMillis: Long = 0L - - /** 服务端 CooFX scene 数。 */ - @CodecField var cooFxScenes: Int = 0 - - /** 服务端 Composition 数。 */ - @CodecField var compositions: Int = 0 - - /** 服务端 DisplayEntity 数。 */ - @CodecField var displayEntities: Int = 0 - - /** 服务端 Emitter 数。 */ - @CodecField var emitters: Int = 0 - - /** 服务端 JVM 累计垃圾收集次数。 */ - @CodecField var gcCollectionCount: Long = 0L - - /** 服务端 JVM 累计垃圾收集耗时毫秒。 */ - @CodecField var gcCollectionTimeMs: Long = 0L - - /** 服务端最大堆字节数。 */ - @CodecField var heapMaxBytes: Long = 0L - - /** 服务端已使用堆字节数。 */ - @CodecField var heapUsedBytes: Long = 0L - - /** 服务端历史最大 MSPT。 */ - @CodecField var maxMspt: Double = 0.0 - - /** 服务端累计收到的 CooPacket 业务字节数。 */ - @CodecField var networkReceivedBytes: Long = 0L - - /** 服务端累计收到的 CooPacket 数。 */ - @CodecField var networkReceivedPackets: Long = 0L - - /** 服务端累计发送的 CooPacket 业务字节数。 */ - @CodecField var networkSentBytes: Long = 0L - - /** 服务端累计发送的 CooPacket 数。 */ - @CodecField var networkSentPackets: Long = 0L - - /** 当前在线玩家数。 */ - @CodecField var onlinePlayers: Int = 0 - - /** 服务端历史 P95 MSPT。 */ - @CodecField var p95Mspt: Double = 0.0 - - /** 服务端旧 ParticleGroup 数。 */ - @CodecField var particleGroups: Int = 0 - - /** 服务端配置的 Status 快照刷新间隔。 */ - @CodecField var refreshIntervalTicks: Int = 20 - - /** 服务端 RenderEntity 数。 */ - @CodecField var renderEntities: Int = 0 - - /** 服务端当前 tick 序号。 */ - @CodecField var serverTick: Long = 0L - - /** 服务端管理的声音实例数。 */ - @CodecField var soundInstances: Int = 0 - - /** 服务端管理的循环声音数。 */ - @CodecField var soundLoops: Int = 0 - - /** 当前目标 TPS。 */ - @CodecField var targetTps: Double = 20.0 - - /** 服务端 Terrain effect group 数。 */ - @CodecField var terrainEffectGroups: Int = 0 - - /** 服务端 Terrain mapping 实例数。 */ - @CodecField var terrainMappings: Int = 0 - - /** 当前可维持 TPS。 */ - @CodecField var tps: Double = 20.0 - - /** 服务端原版 Connection 累计收到的 Packet 数。 */ - @CodecField var vanillaNetworkReceivedPackets: Long = 0L - - /** 服务端原版 Connection 累计发送的 Packet 数。 */ - @CodecField var vanillaNetworkSentPackets: Long = 0L - - /** 返回该业务包的稳定协议 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** 把协议字段恢复为不可变服务端快照。 */ - fun toSnapshot(): PerformanceStatusServerSnapshot { - return PerformanceStatusServerSnapshot( - capturedAtEpochMillis = capturedAtEpochMillis, - serverTick = serverTick, - refreshIntervalTicks = refreshIntervalTicks.coerceIn(5, 1_200), - targetTps = targetTps, - tps = tps, - averageMspt = averageMspt, - p95Mspt = p95Mspt, - maxMspt = maxMspt, - onlinePlayers = onlinePlayers, - particleGroups = particleGroups, - renderEntities = renderEntities, - displayEntities = displayEntities, - emitters = emitters, - compositions = compositions, - gcCollectionCount = gcCollectionCount, - gcCollectionTimeMs = gcCollectionTimeMs, - terrainEffectGroups = terrainEffectGroups, - terrainMappings = terrainMappings, - soundInstances = soundInstances, - soundLoops = soundLoops, - barrages = barrages, - cooFxScenes = cooFxScenes, - heapUsedBytes = heapUsedBytes, - heapMaxBytes = heapMaxBytes, - cooPackets = PerformanceStatusNetworkTotals( - sentPackets = networkSentPackets, - sentBytes = networkSentBytes, - receivedPackets = networkReceivedPackets, - receivedBytes = networkReceivedBytes, - ), - vanillaPackets = PerformanceStatusVanillaPacketTotals( - sentPackets = vanillaNetworkSentPackets, - receivedPackets = vanillaNetworkReceivedPackets, - ), - ) - } - - /** 协议注册常量和快照转换入口。 */ - companion object { - /** 业务包稳定注册 ID,属于网络协议的一部分。 */ - private const val PACKET_ID = "performance_status_snapshot_s2c" - - /** 从不可变服务端快照创建可反射编码的业务包。 */ - fun from(snapshot: PerformanceStatusServerSnapshot): PacketPerformanceStatusSnapshotS2C { - return PacketPerformanceStatusSnapshotS2C().also { packet -> - packet.averageMspt = snapshot.averageMspt - packet.barrages = snapshot.barrages - packet.capturedAtEpochMillis = snapshot.capturedAtEpochMillis - packet.cooFxScenes = snapshot.cooFxScenes - packet.compositions = snapshot.compositions - packet.displayEntities = snapshot.displayEntities - packet.emitters = snapshot.emitters - packet.gcCollectionCount = snapshot.gcCollectionCount - packet.gcCollectionTimeMs = snapshot.gcCollectionTimeMs - packet.heapMaxBytes = snapshot.heapMaxBytes - packet.heapUsedBytes = snapshot.heapUsedBytes - packet.maxMspt = snapshot.maxMspt - packet.networkReceivedBytes = snapshot.cooPackets.receivedBytes - packet.networkReceivedPackets = snapshot.cooPackets.receivedPackets - packet.networkSentBytes = snapshot.cooPackets.sentBytes - packet.networkSentPackets = snapshot.cooPackets.sentPackets - packet.onlinePlayers = snapshot.onlinePlayers - packet.p95Mspt = snapshot.p95Mspt - packet.particleGroups = snapshot.particleGroups - packet.refreshIntervalTicks = snapshot.refreshIntervalTicks - packet.renderEntities = snapshot.renderEntities - packet.serverTick = snapshot.serverTick - packet.soundInstances = snapshot.soundInstances - packet.soundLoops = snapshot.soundLoops - packet.targetTps = snapshot.targetTps - packet.terrainEffectGroups = snapshot.terrainEffectGroups - packet.terrainMappings = snapshot.terrainMappings - packet.tps = snapshot.tps - packet.vanillaNetworkReceivedPackets = snapshot.vanillaPackets.receivedPackets - packet.vanillaNetworkSentPackets = snapshot.vanillaPackets.sentPackets - } - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt deleted file mode 100644 index ba8c6d2c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt +++ /dev/null @@ -1,48 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.BoundControllerEntry -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketOpenBoundTestSelectionScreenS2C() : CooPacket() { - @CodecField var currentIndices: List = emptyList() - @CodecField var currentOptionIds: List = emptyList() - @CodecField var dimensions: List = emptyList() - @CodecField var groupIds: List = emptyList() - @CodecField var loaded: List = emptyList() - @CodecField var optionCounts: List = emptyList() - @CodecField var positions: List = emptyList() - @CodecField var running: List = emptyList() - @CodecField var statuses: List = emptyList() - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_bound_test_selection_screen_s2c") - } - - override fun onClientReceive(context: ClientContext) { - TestControllerClientScreens.openBoundSelection(this) - } - - companion object { - fun fromEntries(entries: List): PacketOpenBoundTestSelectionScreenS2C { - return PacketOpenBoundTestSelectionScreenS2C().also { - it.dimensions = entries.map { entry -> entry.dimension } - it.positions = entries.map { entry -> entry.pos } - it.groupIds = entries.map { entry -> entry.groupId } - it.statuses = entries.map { entry -> entry.status } - it.currentIndices = entries.map { entry -> entry.currentIndex } - it.currentOptionIds = entries.map { entry -> entry.currentOptionId } - it.optionCounts = entries.map { entry -> entry.optionCount } - it.loaded = entries.map { entry -> entry.loaded } - it.running = entries.map { entry -> entry.running } - } - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt deleted file mode 100644 index 3139e246..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt +++ /dev/null @@ -1,123 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockEntity -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens -import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec -import cn.coostack.cooparticlesapi.test.api.TestOptionParamSpec -import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketOpenTestControllerScreenS2C() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var boxDepth: Double = 0.6 - @CodecField var boxHeight: Double = 1.8 - @CodecField var boxWidth: Double = 0.6 - @CodecField var currentIndex: Int = 0 - @CodecField var dimension: String = "" - @CodecField var forwardX: Double = 0.0 - @CodecField var forwardY: Double = 0.0 - @CodecField var forwardZ: Double = 1.0 - @CodecField var forwardDynamic: Boolean = false - @CodecField var forwardTrack: String = "" - @CodecField var groupId: String = "" - @CodecField var mode: String = "sequential" - @CodecField var offsetX: Double = 0.0 - @CodecField var offsetY: Double = 0.0 - @CodecField var offsetZ: Double = 0.0 - @CodecField var optionCount: Int = 0 - @CodecField var optionIds: List = emptyList() - @CodecField var optionParamSpecs: List = emptyList() - @CodecField var optionParamValues: List = emptyList() - @CodecField var pendingReview: Boolean = false - @CodecField var positionDynamic: Boolean = false - @CodecField var positionTrack: String = "" - @CodecField var registeredIds: List = emptyList() - @CodecField var registeredOptionIds: List = emptyList() - @CodecField var registeredOptionParamSpecs: List> = emptyList() - @CodecField var repeatDelayTicks: Int = 0 - @CodecField var repeatIndex: Boolean = false - @CodecField var running: Boolean = false - @CodecField var selectedIndex: Int = 0 - @CodecField var status: String = "" - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_test_controller_screen_s2c") - } - - override fun onClientReceive(context: ClientContext) { - TestControllerClientScreens.openController(this) - } - - companion object { - /** - * 从服务端方块实体创建控制界面快照。 - * - * @param dimension 方块所在维度的资源 ID 文本 - * @param blockEntity 待同步的测试控制器方块实体 - * @param registeredIds 当前可用的方块测试组资源 ID - * @return 可发送到客户端的完整界面数据包 - */ - fun fromBlockEntity( - dimension: String, - blockEntity: TestControllerBlockEntity, - registeredIds: List - ): PacketOpenTestControllerScreenS2C { - return PacketOpenTestControllerScreenS2C().also { - it.dimension = dimension - it.blockPos = blockEntity.blockPos - it.groupId = blockEntity.groupId - it.mode = blockEntity.mode.id - it.selectedIndex = blockEntity.selectedIndex - it.repeatIndex = blockEntity.repeatIndex - it.repeatDelayTicks = blockEntity.repeatDelayTicks - it.offsetX = blockEntity.playerOffset.x - it.offsetY = blockEntity.playerOffset.y - it.offsetZ = blockEntity.playerOffset.z - it.forwardX = blockEntity.playerForward.x - it.forwardY = blockEntity.playerForward.y - it.forwardZ = blockEntity.playerForward.z - it.forwardDynamic = blockEntity.playerForwardDynamic - it.forwardTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerForwardTrack) - it.positionDynamic = blockEntity.playerPositionDynamic - it.positionTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerPositionTrack) - it.boxWidth = blockEntity.playerBoxWidth - it.boxHeight = blockEntity.playerBoxHeight - it.boxDepth = blockEntity.playerBoxDepth - it.running = blockEntity.isRunning() - it.status = blockEntity.statusText() - it.optionCount = blockEntity.optionCount() - it.optionIds = ArrayList(blockEntity.optionIds()) - it.optionParamSpecs = encodeOptionParamSpecs(blockEntity.optionParamSpecs()) - it.optionParamValues = TestOptionParamCodec.encodeAllOptionValues(blockEntity.optionParamValues()) - it.pendingReview = blockEntity.hasPendingReview() - it.currentIndex = blockEntity.currentIndex() - it.registeredIds = registeredIds.map(ResourceLocation::toString) - it.registeredOptionIds = ArrayList(registeredIds.map { id -> - encodeOptionIds(blockEntity.optionIds(id)) - }) - it.registeredOptionParamSpecs = ArrayList(registeredIds.map { id -> - encodeOptionParamSpecs(blockEntity.optionParamSpecs(id)) - }) - } - } - - private fun encodeOptionIds(optionIds: List): String { - return optionIds.joinToString(OPTION_ID_SEPARATOR) - } - - private fun encodeOptionParamSpecs(optionSpecs: List>>): List { - return TestOptionParamCodec.encodeAllOptionSpecs(optionSpecs).map { encoded -> - encoded.encodeToByteArray() - } - } - - const val OPTION_ID_SEPARATOR: String = "\u001F" - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt deleted file mode 100644 index 3d3ac4b0..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import cn.coostack.cooparticlesapi.test.block.BlockTestOptionResult -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketReviewTestControllerC2S() : CooPacket() { - @CodecField var action: String = "" - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos, action: String) : this() { - this.dimension = dimension - this.blockPos = blockPos - this.action = action - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "review_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - val result = when { - action.equals(PASS, ignoreCase = true) -> BlockTestOptionResult.PASSED - action.equals(FAIL, ignoreCase = true) -> BlockTestOptionResult.FAILED - action.equals(SKIP, ignoreCase = true) -> BlockTestOptionResult.SKIPPED - else -> return - } - TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.reviewCurrent(result) - } - - companion object { - const val PASS = "pass" - const val FAIL = "fail" - const val SKIP = "skip" - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt deleted file mode 100644 index 39c2c610..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketSelectBoundTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos) : this() { - this.dimension = dimension - this.blockPos = blockPos - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "select_bound_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return - TestControllerBlockAccess.openControllerScreen(sender, blockEntity) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt deleted file mode 100644 index 39a58572..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketStartTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos) : this() { - this.dimension = dimension - this.blockPos = blockPos - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "start_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.startTest() - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt deleted file mode 100644 index a0389b03..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketStopTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos) : this() { - this.dimension = dimension - this.blockPos = blockPos - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "stop_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.stopTest() - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt deleted file mode 100644 index 5fe02c17..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt +++ /dev/null @@ -1,80 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec -import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec -import cn.coostack.cooparticlesapi.test.block.BlockTestMode -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 - -@CooAutoRegister -class PacketUpdateTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var boxDepth: Double = 0.6 - @CodecField var boxHeight: Double = 1.8 - @CodecField var boxWidth: Double = 0.6 - @CodecField var dimension: String = "" - @CodecField var forwardX: Double = 0.0 - @CodecField var forwardY: Double = 0.0 - @CodecField var forwardZ: Double = 1.0 - @CodecField var forwardDynamic: Boolean = false - @CodecField var forwardTrack: String = "" - @CodecField var groupId: String = "" - @CodecField var mode: String = "sequential" - @CodecField var offsetX: Double = 0.0 - @CodecField var offsetY: Double = 0.0 - @CodecField var offsetZ: Double = 0.0 - @CodecField var optionParamIndex: Int = 0 - @CodecField var optionParamValues: String = "" - @CodecField var positionDynamic: Boolean = false - @CodecField var positionTrack: String = "" - @CodecField var repeatDelayTicks: Int = 0 - @CodecField var repeatIndex: Boolean = false - @CodecField var reopen: Boolean = false - @CodecField var selectedIndex: Int = 0 - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "update_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return - val wasRunning = blockEntity.isRunning() - // 待人工复核时重启会丢弃尚未给出的复核结果,交由随后的复核包决定测试组去向。 - val pendingReview = blockEntity.hasPendingReview() - val offset = Vec3(offsetX, offsetY, offsetZ) - val forward = Vec3(forwardX, forwardY, forwardZ) - val changed = blockEntity.updateConfig( - groupId = groupId, - mode = BlockTestMode.fromId(mode), - selectedIndex = selectedIndex, - repeatIndex = repeatIndex, - repeatDelayTicks = repeatDelayTicks, - playerOffset = offset, - playerForward = forward, - playerPositionDynamic = positionDynamic, - playerForwardDynamic = forwardDynamic, - playerPositionTrack = BlockTestAnimationTrackCodec.decode(positionTrack, offset), - playerForwardTrack = BlockTestAnimationTrackCodec.decode(forwardTrack, forward), - playerBoxWidth = boxWidth, - playerBoxHeight = boxHeight, - playerBoxDepth = boxDepth, - optionParamIndex = optionParamIndex, - optionParamValues = TestOptionParamCodec.decodeOptionValues(optionParamValues) - ) - if (wasRunning && changed && !pendingReview) { - blockEntity.startTest() - } - if (reopen) { - TestControllerBlockAccess.openControllerScreen(sender, blockEntity) - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt deleted file mode 100644 index 2f0ae949..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt +++ /dev/null @@ -1,164 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.utils.MathDataUtil - -/** - * 由于服务端获取不到客户层的粒子总数 - * 所以这里不做粒子上限 - * 但是记录是用于 其他客户端同步 - * - * 如果要设定自动同步 - * 在 otherPacketArgs 设置 "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount) - * 或者 在这个方法中 return mapOf(toggleArgLeastIndex(),toggleArgStatus(),...其他自定义的数据) - * - * 你无需在 SequencedServerParticleGroup的Provider中 手动应用toggle输入的count - * 服务器会自动处理此参数 - * - * 数据包设定 - * addCount args type int -> count - * removeCount args type int -> count - * toggle args type int-> count - */ -@Deprecated("使用ParticleComposition") -abstract class SequencedServerParticleGroup(visibleRange: Double = 32.0) : ServerParticleGroup(visibleRange) { - /** - * 记录了每一个粒子的显示状态 (只要maxCount确实等于loadParticleLocationsWithIndex返回的map的size) - */ - private val clientIndexStatus = LongArray((maxCount() / 64) + 1) - - var serverSequencedParticleCount = 0 - - /** - * 切记一定要和 SequencedParticleGroup.loadParticleLocationsWithIndex().size 相同 - * - * 如果你的group的粒子数量是可变的(使用了flush方法刷新了粒子样式 其中长度发生变化) - * - * 那么请在服务器层做好数据同步 ( size同步 ) - * - * 如果此处的 maxCount > SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致数组越界异常 - * - * 如果此处的 maxCount < SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致粒子控制不完全(部分粒子无法从服务器生成) - * - * @return 应当返回 SequencedParticleGroup.loadParticleLocationsWithIndex().size - */ - abstract fun maxCount(): Int - - fun isDisplayed(index: Int): Boolean { - val storage = MathDataUtil.getStoragePageLong(index) - if (storage >= clientIndexStatus.size) { - return false - } - - return MathDataUtil.getStorageWithBitLong( - MathDataUtil.getStorageWithBitLong(index) - ) == 1 - } - - fun setDisplayed(index: Int, status: Boolean) { - val storage = MathDataUtil.getStorageWithBitLong(index) - val page = MathDataUtil.getStoragePageLong(index) - if (page >= clientIndexStatus.size) { - return - } - clientIndexStatus[page] = MathDataUtil.setStatusLong(clientIndexStatus[page], storage, status) - } - - fun toggleArgLeastIndex(): Pair> { - return "toggle" to ParticleControlerDataBuffers.int(1) - } - - fun toggleArgStatus(): Pair> { - return "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) - } - - fun addSingle() { - if (serverSequencedParticleCount >= maxCount()) { - return - } - change( - { - setDisplayed(serverSequencedParticleCount++, true) - }, - mapOf( - "addCount" to ParticleControlerDataBuffers.int(1) - ) - ) - } - - fun addMultiple(count: Int) { - change( - { - for (index in serverSequencedParticleCount until serverSequencedParticleCount + count) { - setDisplayed(index, true) - } - serverSequencedParticleCount += count - }, - mapOf( - "addCount" to ParticleControlerDataBuffers.int(count) - ) - ) - if (serverSequencedParticleCount >= maxCount()) { - serverSequencedParticleCount = maxCount() - } - } - - fun removeSingle(): Boolean { - if (serverSequencedParticleCount <= 0) { - return false - } - change( - { - setDisplayed(serverSequencedParticleCount--, false) - }, - mapOf( - "removeCount" to ParticleControlerDataBuffers.int(1) - ) - ) - return true - } - - fun removeMultiple(count: Int) { - if (serverSequencedParticleCount <= 0) { - return - } - change( - { - for (index in serverSequencedParticleCount - count + 1..serverSequencedParticleCount) { - setDisplayed(index, false) - } - serverSequencedParticleCount -= count - if (serverSequencedParticleCount <= 0) { - serverSequencedParticleCount = 0 - } - }, - mapOf( - "removeCount" to ParticleControlerDataBuffers.int(count) - ) - ) - } - - fun changeSingle(index: Int, status: Boolean) { - if (index >= maxCount() || index < 0) { - return - } - change( - { - setDisplayed(index, status) - }, mapOf( - "change_single" to ParticleControlerDataBuffers.int(index) - ) - ) - } - - fun toggleCurrentCount() { - change( - {}, - mapOf( - "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount), - "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) - ) - ) - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt deleted file mode 100644 index f515823d..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt +++ /dev/null @@ -1,328 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.api.controler.server.ServerControler -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.entity.LivingEntity -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -/** - * @param clientGroup 客户端渲染的类型 - * @param uuid 粒子组合唯一标识符 - * @param visibleRange 玩家可见范围 (origin) - */ -@Deprecated("使用ParticleComposition") - -abstract class ServerParticleGroup( - var visibleRange: Double = 32.0 -) : ServerControler { - val uuid: UUID = UUID.randomUUID() - var pos: Vec3 = Vec3.ZERO - internal set - var world: Level? = null - internal set - internal var valid = true - var canceled = false - internal set - - /** - * 客户端渲染视角下的生命周期 - */ - var clientTick = 0 - internal set - var clientMaxTick = 120 - internal set - var scale = 1.0 - - /** - * 服务器内的生命周期 - * 同步数据时不会传输给客户端 - */ - var tick = 0 - var maxTick = 120 - - - var axis: RelativeLocation = RelativeLocation(0.0, 1.0, 0.0) - internal set - - /** - * 每个tick能做的事情 - * 例如绑定实体位置 旋转等 - */ - abstract fun tick() - - /** - * 获取自定义包的其他参数 (用于服务器传输给客户端) - */ - abstract fun otherPacketArgs(): Map> - - /** - * 获取对应的客户端层Class - * @return null 不能单独生成的类型 - */ - abstract fun getClientType(): Class? - - - /** - * 当粒子组合在服务器创建并且发送到客户端时执行 - * 可以给客户端发送一些包修改 - */ - open fun onGroupDisplay(pos: Vec3, world: ServerLevel) {} - - /** - * 通过doTickAlive函数导致粒子组合到达最终生命周期时执行的函数 - * 不强制重写 - */ - open fun onTickAliveDeath() {} - - /** - * 同步后客户端层面粒子死亡 (除非没开启tickAlive) - * 服务器处理上同样 - */ - open fun onClientViewDeath() {} - - /** - * 可以在abstract tick中执行 - */ - fun doTickClient() { - if (clientTick++ >= clientMaxTick) { - onClientViewDeath() - } - } - - /** - * 可以在abstract tick中执行 - */ - fun doTickAlive() { - if (tick++ >= maxTick) { - kill() - onTickAliveDeath() - } - } - - /** - * 不立刻同步给客户端 - * 只修改参数 - * 但是在新加入可视的玩家会应用此scale - * 如果想要在客户端不应用服务器的scale - * 则需要在provider的create方法中重新修改scale值 - */ - fun scaleOnServer(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale must be greater than zero.") - return - } - this.scale = new - } - - /** - * 服务器修改客户端缩放大小 - * 并且立刻同步到客户端 - */ - fun scale(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale must be greater than zero.") - return - } - change( - { - this.scale = new - }, - mapOf( - PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(new) - ) - ) - } - - /** - * 销毁包 (在update时会删除所有的可见) - */ - fun kill() { - canceled = true - world ?: return - val visible = ServerParticleGroupManager.filterVisiblePlayer(this) - visible.forEach { - val player = world!!.getPlayerByUUID(it) ?: return@forEach - // 发销毁包 - val packet = PacketParticleGroupS2C(uuid, ControlType.REMOVE, mapOf()) - CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) - } - } - - /** - * 跟随玩家在线状态. 世界跟换 删除粒子 - */ - fun withPlayerStats(player: ServerPlayer) { - if (player.hasDisconnected()) { - kill() - } - - if (player.level() != world) { - kill() - } - } - - fun withEntityStats(entity: LivingEntity) { - if (entity.isDeadOrDying) { - kill() - } - - if (entity.level() != world) { - kill() - } - - } - - fun setAxis(axis: Vec3) { - change( - { this.axis = RelativeLocation.of(axis) }, mapOf( - PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(axis) - ) - ) - } - - /** - * 不发送到客户端 - */ - fun setPosOnServer(pos: Vec3) { - this.pos = pos - } - - /** - * 同步旋转后的中心点和对称轴 - */ - fun setRotateToOnServer(to: RelativeLocation) { - axis = to.normalize() - } - - - /** - * 出现了一些控制类之外的传送 - */ - fun teleportGroupTo(pos: Vec3) { - // 发包告知 所有可见客户端 - change( - { this.pos = pos }, mapOf( - PacketParticleGroupS2C.PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(pos) - ) - ) - } - - override fun getValue(): ServerParticleGroup { - return this - } - - override fun remove() { - kill() - } - - - override fun teleportTo(to: Vec3) { - teleportGroupTo(to) - } - - override fun teleportTo(x: Double, y: Double, z: Double) { - teleportTo(Vec3(x, y, z)) - } - - /** - * 出现了一些控制类之外的旋转 - */ - override fun rotateAsAxis(angle: Double) { - change( - {}, mapOf( - PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( - angle - ) - ) - ) - } - - override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { - change( - {}, mapOf( - PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( - angle - ), - PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d( - to.toVector() - ) - ) - ) - } - - override fun spawn(world: Level, pos: Vec3) { - if (world !is ServerLevel) return - ServerParticleGroupManager.addParticleGroup(this, pos, world) - } - - /** - * 出现了一些控制类之外的旋转 - */ - override fun rotateToPoint(to: RelativeLocation) { - rotateParticlesToPoint(to.toVector()) - } - - fun rotateParticlesToPoint(to: Vec3) { - change( - {}, mapOf( - PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d(to) - ) - ) - } - - fun changeTick(tick: Int) { - change( - { clientTick = tick }, mapOf( - PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int( - tick - ) - ) - ) - } - - fun changeMaxTick(tick: Int) { - change( - { this.maxTick = tick }, mapOf( - PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(tick) - ) - ) - } - - /** - * 发送行为修改包 - * @param toggleMethod 修改Server层的参数的方法 如果不用此修改方法则会导致其他客户端渲染不同步 - * @param args 此参数出现过的键 必须在otherPacketArgs方法返回的args中存在 否则任然会存在同步问题 - */ - fun change(toggleMethod: ServerParticleGroup.() -> Unit, args: Map>) { - world ?: return - val visible = ServerParticleGroupManager.filterVisiblePlayer(this) - toggleMethod(this) - visible.forEach { - val player = world!!.getPlayerByUUID(it) ?: return@forEach - val packet = PacketParticleGroupS2C(uuid, ControlType.CHANGE, args) - CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) - } - } - - fun initServerGroup(pos: Vec3, world: Level, maxTick: Int = 120) { - this.pos = pos - this.world = world - this.clientMaxTick = maxTick - } - - override fun isValid(): Boolean { - return valid - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt deleted file mode 100644 index 1588441b..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt +++ /dev/null @@ -1,200 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle - -import cn.coostack.cooparticlesapi.CooParticlesAPI -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C.PacketArgsType -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.phys.Vec3 -import java.util.* -import java.util.concurrent.ConcurrentHashMap - - -/** - * 控制所有的ServerParticleGroup - */ -@Deprecated("使用ParticleComposition") - -object ServerParticleGroupManager { - private val serverGroups = ConcurrentHashMap() - - /** - * key playerUUID - * value player can see - */ - internal val visible = ConcurrentHashMap>() - - fun addParticleGroup( - group: ServerParticleGroup, - pos: Vec3, - world: ServerLevel - ) { - serverGroups[group.uuid] = group - group.initServerGroup(pos, world) - world.players().filter { it.position().distanceTo(pos) <= group.visibleRange } - .forEach { receiver -> - addGroupPlayerView(receiver, group) - } - group.onGroupDisplay(pos, world) - } - - fun getParticleGroup(group: UUID): ServerParticleGroup? { - return serverGroups[group] - } - - fun getGroups(): Map { - return Collections.unmodifiableMap(serverGroups) - } - - /** 返回服务端当前旧 ParticleGroup 实例数。 */ - fun groupCount(): Int = serverGroups.size - - fun clearServer() { - serverGroups.onEach { it.value.canceled = true }.clear() - visible.clear() - } - - fun upgrade() { - upgradeGroups() - clearOfflineVisible() - } - - fun filterVisiblePlayer(group: ServerParticleGroup): Set { - val set = HashSet() - visible.forEach { - if (group in it.value) { - set.add(it.key) - } - } - return set - } - - private fun upgradeGroups() { - val iterator = serverGroups.iterator() - while (iterator.hasNext()) { - val value = iterator.next().value - // 更新状态 - if (value.canceled || !value.valid) { - val server = value.world?.server ?: continue - visible.forEach { (t, u) -> - val player = server.playerList.getPlayer(t) ?: return@forEach - if (value in u) { - removeGroupPlayerView(player, value) - u.remove(value) - } - } - iterator.remove() - continue - } - // 更新可见性 - value.world!!.server!!.playerList.players.forEach { p -> - val visibleSet = visible.getOrPut(p.uuid) { HashSet() } - if (p.level().dimension() != value.world?.dimension()) { - // 世界转换 - if (value in visibleSet) { - removeGroupPlayerView(p, value) - } - visibleSet!!.remove(value) - return@forEach - } - if (p.isDeadOrDying) { - if (value in visibleSet) { - removeGroupPlayerView(p, value) - visibleSet!!.remove(value) - } - return@forEach - } - if (value.pos.distanceTo(p.position()) <= value.visibleRange) { - // 防止重复添加(发包) - if (value in visibleSet) { - return@forEach - } - addGroupPlayerView(p, value) - // 同步数据包 - togglePacketView(p, value) - - } else { - if (value in visibleSet) { - removeGroupPlayerView(p, value) - } - // 超过范围 - visibleSet.remove(value) - } - } - value.tick() - } - } - - private fun clearOfflineVisible() { - val server = CooParticlesAPI.serverOrNull ?: return - // 清空所有离线玩家 - val visibleIterator = visible.iterator() - while (visibleIterator.hasNext()) { - val entry = visibleIterator.next() - val player = server.playerList.getPlayer(entry.key) - if (player == null || player.hasDisconnected()) { - visibleIterator.remove() - } - } - } - - private fun togglePacketView(target: ServerPlayer, group: ServerParticleGroup) { - val packet = PacketParticleGroupS2C( - group.uuid, ControlType.CHANGE, - mutableMapOf( - PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(group.pos), - PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(group.axis.toVector()), - PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientTick), - PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientMaxTick), - ) - ) - - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - /** - * 在该玩家的视角中移除这些粒子 - * @param target - * @param targetGroup - */ - private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ServerParticleGroup) { - // 发包给玩家 - val packet = PacketParticleGroupS2C( - targetGroup.uuid, - ControlType.REMOVE, mapOf() - ) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - /* - * @param target - * @param targetGroup - */ - private fun addGroupPlayerView( - target: ServerPlayer, - targetGroup: ServerParticleGroup, - ) { - // 修复一个包发送给一个玩家两次 - val visibleSet = visible.getOrPut(target.uuid) { HashSet() } - visibleSet.add(targetGroup) - // 发包给玩家 - val packet = PacketParticleGroupS2C( - targetGroup.uuid, - ControlType.CREATE, - mutableMapOf( - PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(targetGroup.pos), - PacketArgsType.GROUP_TYPE.ofArgs to ParticleControlerDataBuffers.string(targetGroup.getClientType()!!.name), - PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientTick), - PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientMaxTick), - PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(targetGroup.scale) - ).apply { - putAll(targetGroup.otherPacketArgs()) - } - ) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt deleted file mode 100644 index bf4c43db..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoParticleComposition.kt +++ /dev/null @@ -1,18 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -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(): StreamCodec { - return ParticleCompositionRegistryHelper.generateCodec(this) - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt deleted file mode 100644 index 013746cd..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/AutoSequencedParticleComposition.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.annotations.composition.handler.ParticleCompositionRegistryHelper -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -abstract class AutoSequencedParticleComposition(position: Vec3, world: Level? = null) : - SequencedParticleComposition(position, world) { - - override fun getCodec(): StreamCodec { - return ParticleCompositionRegistryHelper.generateCodec(this) - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt deleted file mode 100644 index 2b7be814..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt +++ /dev/null @@ -1,51 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.cparticle.CParticle -import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.particles.control.ParticleControler -import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect -import java.util.UUID - -class CompositionData : Comparable { - var order = 0 - val uuid = UUID.randomUUID() - val singleParticleHandlers = ArrayList Unit>() - val particleControlerHandlers = ArrayList Unit>() - val cParticleHandlers = ArrayList Unit>() - val cParticleControlerHandlers = ArrayList Unit>() - var displayerBuilder: (UUID) -> ParticleDisplayer = { - ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) - } - private set - - fun setDisplayerSupplier(supplier: (UUID) -> ParticleDisplayer): CompositionData { - displayerBuilder = supplier - return this - } - - fun addParticleInstanceInit(init: ControlableParticle.() -> Unit): CompositionData { - singleParticleHandlers.add(init) - return this - } - - fun addParticleControlerInstanceInit(init: ParticleControler.() -> Unit): CompositionData { - particleControlerHandlers.add(init) - return this - } - - fun addCParticleInstanceInit(init: CParticle.() -> Unit): CompositionData { - cParticleHandlers.add(init) - return this - } - - fun addCParticleControlerInstanceInit(init: CParticleControlable.() -> Unit): CompositionData { - cParticleControlerHandlers.add(init) - return this - } - - override fun compareTo(other: CompositionData): Int { - return order - other.order - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt deleted file mode 100644 index da4a7398..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleComposition.kt +++ /dev/null @@ -1,1177 +0,0 @@ -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.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -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 - -/** - * 因为不想写太多的ControlableBuffer, 而改用Codec自动生成数据 (同时避免写一万个Provider内部类) - * - * 所以重构ParticleStyle - * - * 首先强制 auto update - * - * 使用自动注册 - * @see cn.coostack.cooparticlesapi.annotations.composition.ParticleCompositionRegister - */ -abstract class ParticleComposition : ServerControler, - Controlable, Tickable, NetworkDirtyMarkable { - companion object { - @JvmStatic - fun encodeBase(data: ParticleComposition, buf: FriendlyByteBuf) { - 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: FriendlyByteBuf) { - 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 - - /** - * 如果作为子 composition输入就一定要修改这个 - */ - var controlUUID = UUID.randomUUID() - - var axis = RelativeLocation.yAxis() - - var roll = 0.0 - - val particles = ConcurrentHashMap>() - - /** - * 排除掉 SingleParticleDisplayer 从而减少遍历次数 - */ - val controlerTicks = HashSet>() - - val particleLocations = ConcurrentHashMap, RelativeLocation>() - - val status = CompositionStatusHelper() - - /** 当粒子组合初始化时,存储一倍缩放粒子组与原点的距离。 */ - val particleDefaultLength = ConcurrentHashMap() - - /** 保留一倍缩放时的相对向量,使零缩放后的粒子能够恢复方向。 */ - private val particleDefaultLocations = ConcurrentHashMap() - - // 防止频繁的toList造成的性能浪费 - - 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 - - /** - * 标记此 Composition 的完整网络状态需要重新同步。 - * - * 自定义同步字段优先使用 `var value by dirty(initial)`;普通 `@CodecField` 修改后需要显式调用本方法。 - */ - 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(): StreamCodec - - abstract fun getParticles(): Map - - abstract fun onDisplay() - - /** - * 快捷设置 链式调用 - * - * @param interval - * @return - */ - fun setDisabledInterval(interval: Int): ParticleComposition { - this.status.closedInternal = interval - markNetworkStateDirty() - return this - } - - /** - * 配置此 composition 自动创建的所有 CParticle systems。 - * 名称、容量、渲染层、模式和生命周期由 composition 管理。 - */ - fun configureCParticleSystem(configure: CParticleSystem.() -> Unit): ParticleComposition { - return setCParticleSystemConfiguration(null, configure) - } - - /** 只配置指定渲染层的 system。 */ - 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 - } - - /** 返回此 composition 在指定层自动创建的 system;尚未显示时返回 null。 */ - fun getCParticleSystem(layer: CParticleRenderLayer): CParticleSystem? { - return managedCParticleSystems.firstOrNull { !it.released && it.layer == layer } - } - - /** 返回此 composition 当前持有的 systems 快照。 */ - 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 - } - } - - /** - * 对当前 composition 创建的所有 CParticle systems 播放同一段 GPU 视觉过渡。 - * - * 示例:`playCParticleVisualTransition(20F, scaleCurve = curve)` 会统一缩放当前 systems。 - * 禁止:不要用它修改粒子生成时的基础尺寸。 - * - * @param durationTicks 过渡时长,单位 tick - * @param alphaCurve 不透明度倍率曲线 - * @param scaleCurve 等比缩放倍率曲线 - * @param colorFrom 可选起始颜色 - * @param colorTo 可选结束颜色 - * @param mode 过渡结束后的行为 - * @return 当前 composition - */ - @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, - ) - } - - /** - * 播放 composition 级 GPU 视觉过渡,并允许强制重新开始相同配置。 - * - * 示例:`playCParticleVisualTransition(20F, true, scaleCurve = curve)` 会重置进度。 - * 禁止:不要用它修改粒子生成时的基础尺寸。 - * - * @param durationTicks 过渡时长,单位 tick - * @param restart 是否强制从头播放 - * @param alphaCurve 不透明度倍率曲线 - * @param scaleCurve 等比缩放倍率曲线 - * @param colorFrom 可选起始颜色 - * @param colorTo 可选结束颜色 - * @param mode 过渡结束后的行为 - * @return 当前 composition - */ - @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, - ) - } - - /** - * 把同一组过渡参数分发给当前 composition 管理的 systems。 - * - * 示例:公开重载通过本方法统一传递 [restart]。 - * 禁止:不要在这里缓存已经释放的 system。 - * - * @param durationTicks 过渡时长,单位 tick - * @param alphaCurve 不透明度倍率曲线 - * @param scaleCurve 等比缩放倍率曲线 - * @param colorFrom 可选起始颜色 - * @param colorTo 可选结束颜色 - * @param mode 过渡结束后的行为 - * @param restart 是否强制从头播放 - * @return 当前 composition - */ - 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 - } - - /** 停止当前 composition 的 CParticle GPU 视觉过渡。 */ - @JvmOverloads - fun stopCParticleVisualTransition(reset: Boolean = false): ParticleComposition { - getCParticleSystems().forEach { it.stopVisualTransition(reset) } - return this - } - - /** - * 扫描此 composition 的完整运行时子树,为所有 CParticle systems 播放同一段 GPU alpha 过渡。 - * 子树包括嵌套 composition、ParticleGroupStyle 和 ControlableParticleGroup。 - * 过渡曲线会覆盖粒子实例的 alpha,再与粒子、system 和普通视觉过渡的 alpha 曲线相乘。 - * - * @param durationTicks 过渡时长,单位为 tick,必须为有限正数 - * @param alphaCurve 用于覆盖粒子实例 alpha 的曲线 - * @param mode 过渡结束后的行为 - * @param restart 是否强制替换相同配置 - * @return 当前 composition - * @throws IllegalArgumentException 当 [durationTicks] 不是有限正数时抛出 - */ - @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, - ) - } - - /** 停止此 composition 完整运行时子树中的 CParticle alpha 过渡。 */ - @JvmOverloads - 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() - // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 - // 多为服务端(除非有人使在Client环境创建了这个类) - if (displayed) { - if (gpuTransformActive) { - applyGpuScale(new) - } else { - toggleScaleDisplayed() - } - } - // 发包有效 - if (!canceled) { - // remove过后 无法同步 - 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 - } - - /** - * 清除当前显示内容,并按 [cancel] 决定是否结束本次生命周期。 - * - * 示例:`clear(true)` 会销毁子节点并从客户端活动计数中移除本实例。 - * 禁止用 `clear(false)` 表示最终移除,因为该模式用于保留 displayed 状态后重新生成内容。 - * - * @param cancel `true` 表示结束生命周期,`false` 表示仅刷新显示内容 - */ - 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) - } - } - - /** - * 在服务端重新生成 Composition 前重置旧生命周期状态。 - * - * 示例:同一实例再次调用 `spawn(...)` 时先执行本方法,避免沿用 canceled 状态。 - * 禁止在仍需保留当前客户端显示内容时调用。 - */ - internal fun resetLifecycleForSpawn() { - ParticleCompositionManager.setClientLoaded(this, false) - canceled = false - displayed = false - networkStateDirty = true - networkFullDirty = true - lastNetworkStatus = status.displayStatus - lastNetworkStatusInterval = status.closedInternal - } - - /** - * 根据当前世界进入服务端同步或客户端显示生命周期。 - * - * 示例:客户端直接显示的嵌套 Composition 会在此登记为活动实例。 - * 禁止在 [world] 尚未设置时调用。 - */ - 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) - } - } - - /** 按一倍缩放缓存恢复方向后应用当前缩放,避免从零缩放恢复时产生 NaN。 */ - 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 - } - // 减少一倍的new Vec3 - 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 - // display - 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() - } - - /** - * 显示一个相对坐标对应的粒子节点。 - * - * 示例:普通粒子以 [position] 为原点,启用整组变换的 managed CParticle 先计算实际渲染坐标。 - * 禁止把 system 局部坐标直接当作世界坐标,否则光照和位置相关纹理会从错误位置采样。 - * - * @param data 粒子显示方式和初始化动作 - * @param pos 已完成初始缩放与旋转的相对坐标 - */ - 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) { - data.cParticleControlerHandlers.forEach { handler -> - handler(controler) - } - } - registerCParticleNode(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) - } - - /** - * 为未绑定的 CParticle displayer 取得当前 composition 专用的 system。 - * - * 示例:同一 composition、渲染层和纹理绑定会复用同一个 system。 - * 禁止覆盖调用方已经绑定的 system;这种情况返回 `null` 并保留原有行为。 - * - * @param displayer 待绑定的 GPU 粒子显示器 - * @return 本次绑定的 managed system;无法绑定或已有外部绑定时返回 `null` - */ - 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 - } - - /** - * 计算粒子显示与环境采样使用的世界坐标。 - * - * 示例:整组变换启用后,序列后续粒子使用 system 当前矩阵换算真实渲染位置。 - * 禁止在这里返回槽位局部坐标;[CParticleSystem] 会在写入时自行逆变换。 - * - * @param pos composition 中保存的相对坐标 - * @param managedSystem 当前 composition 管理的 GPU system,普通显示器传 `null` - * @return 写入显示器的世界坐标 - */ - 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(), - ) - } - - /** - * 返回整组 GPU 变换启用时使用的稳定槽位坐标。 - * - * 示例:system 原点为 `(10, 20, 30)` 时,相对坐标 `(2, 3, 4)` 写为 `(12, 23, 34)`。 - * 禁止在 CPU 路径提供该坐标;此时 system 需要按现存矩阵逆变换真实世界坐标。 - * - * @param pos composition 保存的相对坐标 - * @param managedSystem 当前 composition 管理的 GPU system - * @return 槽位写入坐标;不使用整组 GPU 变换时返回 `null` - */ - 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: Double) { - val axisVector = normalizedAxis(rotationAxis) - val delta = Matrix4f().rotate(radian.toFloat(), 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt deleted file mode 100644 index 38ba6dbc..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/ParticleShapeComposition.kt +++ /dev/null @@ -1,156 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.StatusHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -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 - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - - /** - * 当反转到0时会自动清理粒子 - */ - 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 - } - - /** - * 改用apply是因为 要防止和addPreTickAction冲突 - * - * @return - */ - 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(): StreamCodec { - 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) - } - } - } - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt deleted file mode 100644 index 6ee8e42c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleComposition.kt +++ /dev/null @@ -1,518 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.api.controler.Tickable -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.utils.Math3DUtil -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.helper.SequencedCompositionAnimationHelper -import cn.coostack.cooparticlesapi.utils.storage.Memo -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.UUID -import kotlin.math.max -import kotlin.math.min - -/** - * 自动更新(AutoToggle) 生长动画 - * - * AI神力 根本懒得再写一份Sequenced的版本了 - */ -abstract 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) - - companion object { - @JvmStatic - fun encodeBase(data: SequencedParticleComposition, buf: FriendlyByteBuf) { - ParticleComposition.encodeBase(data, buf) - buf.writeInt(data.count) - buf.writeInt(data.displayedParticleCount) - buf.writeInt(data.serverCurrentIndex) - buf.writeLongArray(data.index.get()) - } - - @JvmStatic - fun decodeBase(instance: SequencedParticleComposition, buf: FriendlyByteBuf) { - ParticleComposition.decodeBase(instance, buf) - instance.count = buf.readInt() - instance.displayedParticleCount = buf.readInt() - instance.serverCurrentIndex = buf.readInt() - instance.index.setMemoValue(buf.readLongArray()) - } - } - - /** - * 这里采用 animate框架 写多个条件用来方便生长动画的条件变化 - * 在init 或者 onDisplay执行 - */ - val animate = - SequencedCompositionAnimationHelper() - .loadComposition(this) - - - var count = 0 - var index = Memo { - var page = count / 64 - if (count % 64 > 0) { - page++ - } - LongArray(page) - } - - /** 服务端:已经“显示”的粒子数量(仅逻辑字段,可用于业务) */ - var displayedParticleCount: Int = 0 - protected set - - /** 服务端:下一个将要生成的 index 指针(用于 addSingle/addMultiple 生长动画) */ - var serverCurrentIndex: Int = 0 - protected set - protected val sequencedParticlesData = ArrayList>() - override fun getParticles(): SortedMap { - return getParticleSequenced() - } - - /** - * 不要调用只有客户端才能调用的方法 因为服务器也会调用一次这个方法用来获取实际的粒子个数 - * - * @return 粒子相对样式 - */ - abstract fun getParticleSequenced(): SortedMap - final override fun tick() { - super.tick() - } - - override fun flush() { - if (particles.isNotEmpty() || indexToUuid.any { it != null }) { - clear(false) - } - displayParticles() - restoreDisplayedParticles() - } - - override fun clear(cancel: Boolean) { - super.clear(cancel) - sequencedParticlesData.clear() - particleRotatedLocations.clear() - indexToUuid.fill(null) - } - - /** - * 进入序列 Composition 的显示生命周期并初始化动画状态。 - * - * 示例:客户端直接显示时会登记为一个活动 Composition 实例。 - * 禁止在 [world] 尚未设置时调用。 - */ - override fun display() { - if (displayed) { - return - } - displayed = true - // 修复禁用后不自动移除的问题 - status.loadControler(this) - status.initHelper() - this.client = world!!.isClientSide - if (client) { - ParticleCompositionManager.setClientLoaded(this, true) - } - // 在服务器需要用来更新粒子个数 所以需要参与一次计算 - flush() - if (!client) { - // 服务器只负责数据同步,不负责粒子生成。 - onDisplay() - return - } - onDisplay() - } - - open fun beforeDisplaySequenced(map: SortedMap) { - - } - - /** - * 这里非常关键: - * - 只计算序列数据、count、缩放/旋转后的相对坐标 - * - 不创建粒子对象(不调用 displayEntry) - */ - override fun displayParticles() { - val locations = getParticles() - val newCount = locations.size - prepareGpuComposition(newCount) - - // 更新粒子总数并扩容位集,同时保留已有状态。 - ensureIndexCapacity(newCount) - - count = newCount - - // 服务端不做任何粒子生成 - if (!client) { - return - } - - beforeDisplaySequenced(locations) - Math3DUtil.rotatePointsToPoint(locations.values.toList(), axis, RelativeLocation.yAxis()) - Math3DUtil.rotateAsAxis(locations.values.toList(), axis, roll) - toggleScale(locations) - - sequencedParticlesData.clear() - sequencedParticlesData.addAll(locations.toList()) - particleRotatedLocations.clear() - particleRotatedLocations.addAll(locations.values) - // 客户端:准备索引到 UUID 的映射数组。 - if (indexToUuid.size != count) { - indexToUuid = arrayOfNulls(count) - } - } - - override fun update(other: ParticleComposition) { - super.update(other) - other as SequencedParticleComposition - val oldIndex = this.index.get().copyOf() - val oldCount = this.count - - this.count = other.count - this.displayedParticleCount = other.displayedParticleCount - this.serverCurrentIndex = other.serverCurrentIndex - - this.index.setMemoValue(other.index.get()) - - if (!client) return - - // 粒子总数或缓存结构变化时,客户端需要重新计算序列数据。 - if (oldCount != this.count || sequencedParticlesData.size != this.count) { - // 缓存重建会移除旧节点,flush 会按新的完整位集恢复已显示状态。 - flush() - return - } - - applyIndexDiff(oldIndex, this.index.get()) - restoreDisplayedParticles() - } - - private fun restoreDisplayedParticles() { - if (!client || sequencedParticlesData.size != count) return - for (i in 0 until count) { - if (isParticleDisplayed(i)) createWithIndex(i) - } - } - - private fun applyIndexDiff(oldBits: LongArray, newBits: LongArray) { - - if (!client) return - val n = count - if (n <= 0) return - if (sequencedParticlesData.size != n) return - - val pages = pagesFor(n) - // 复制到同长度,避免越界判断 - val oldSafe = if (oldBits.size == pages) oldBits else oldBits.copyOf(pages) - val newSafe = if (newBits.size == pages) newBits else newBits.copyOf(pages) - - for (page in 0 until pages) { - var diff = oldSafe[page] xor newSafe[page] - if (diff == 0L) continue - - val base = page shl 6 // 当前页的首个槽位 - while (diff != 0L) { - val bit = diff.countTrailingZeroBits() // 0..63 - val i = base + bit - if (i >= n) break - - val newGen = ((newSafe[page] ushr bit) and 1L) != 0L - if (newGen) createWithIndex(i) else removeWithIndex(i) - - diff = diff and (diff - 1) // 清除最低位的 1 - } - } - } - - - /** - * 服务端:生成下一个粒子(只改 位集 和计数,不生成粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * addSingle() - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断。 - * 方法从当前指针向后寻找第一个未显示槽位,必要时回绕;成功后会标记完整网络状态。 - * 服务端调用必须位于 Composition 生命周期更新所在的逻辑线程。 - */ - fun addSingle() { - if (count <= 0) return - val start = serverCurrentIndex.coerceIn(0, count - 1) - val idx = (start until count).firstOrNull { !isParticleDisplayed(it) } - ?: (0 until start).firstOrNull { !isParticleDisplayed(it) } - ?: return - - setBit(index.get(), idx, true) - displayedParticleCount++ - - // 客户端:立即生成该索引对应的粒子。 - if (client) { - createWithIndex(idx) - } - serverCurrentIndex = min(idx + 1, max(count - 1, 0)) - markDirty() - } - - /** - * 服务端:生成多个粒子(只改 位集 和计数,不生成粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * addMultiple(amount) - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * @param amount 生成数量(小于等于 0 时直接返回) - */ - fun addMultiple(amount: Int) { - if (amount <= 0) return - repeat(amount) { addSingle() } - } - - /** - * 服务端:移除上一个粒子(只改 位集 和计数,不删除粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * removeSingle() - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * 说明: - * - 从 `serverCurrentIndex` 向前寻找最近的已显示槽位,必要时回绕 - * - 成功后清除位、更新计数和指针,并标记完整网络状态 - * - 客户端场景会立即调用 `removeWithIndex`,服务端调用必须位于 Composition 生命周期更新线程 - */ - fun removeSingle() { - if (count <= 0) return - val start = serverCurrentIndex.coerceIn(0, count - 1) - val idx = (start downTo 0).firstOrNull(::isParticleDisplayed) - ?: (count - 1 downTo start + 1).firstOrNull(::isParticleDisplayed) - ?: return - - setBit(index.get(), idx, false) - displayedParticleCount-- - - // 客户端:立即删除该索引对应的粒子。 - if (client) { - removeWithIndex(idx) - } - serverCurrentIndex = max(idx - 1, 0) - markDirty() - } - - /** - * 服务端:移除多个粒子(只改 位集 和计数,不删除粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * removeMultiple(amount) - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * @param amount 移除数量(小于等于 0 时直接返回) - */ - fun removeMultiple(amount: Int) { - if (amount <= 0) return - repeat(amount) { removeSingle() } - } - - /** - * 重置所有粒子状态(清空 位集、计数器、指针) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * resetAll() - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * 说明: - * - 服务端:只清位集、计数和指针,等待同步到客户端后按差异处理 - * - 客户端:先遍历位集,把已生成的粒子对象全部通过 `removeWithIndex(i)` 移除,再清空位集 - * - * 注意: - * - resetAll() 会把 serverCurrentIndex 重置为 0 - * - displayedParticleCount 会重置为 0 - */ - fun resetAll() { - // 客户端:先删除所有已经生成的粒子对象。 - if (client && count > 0) { - val pages = pagesFor(count) - val bits = index.get() - for (page in 0 until min(bits.size, pages)) { - var v = bits[page] - if (v == 0L) continue - val base = page shl 6 - while (v != 0L) { - val bit = v.countTrailingZeroBits() - val i = base + bit - if (i >= count) break - removeWithIndex(i) - v = v and (v - 1) - } - } - } - - // 清空位集。 - val arr = index.get() - val changed = displayedParticleCount != 0 || serverCurrentIndex != 0 || arr.any { it != 0L } - for (i in arr.indices) arr[i] = 0L - - displayedParticleCount = 0 - serverCurrentIndex = 0 - if (changed) markDirty() - } - - override fun rotateToPoint(to: RelativeLocation) { - super.rotateToPoint(to) - } - - override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { - super.rotateToWithAngle(to, radian) - } - - override fun rotateAsAxis(radian: Double) { - super.rotateAsAxis(radian) - } - - /** - * 直接设置一个序列槽位的显示状态。 - * - * 服务端真实变更后会标记完整网络状态,客户端真实变更后会立即创建或移除对应节点。 - * 越界或状态未变化时不会修改计数、节点和网络脏状态。 - * - * @param index 目标槽位索引,有效范围为 `0 until count` - * @param generated `true` 表示显示,`false` 表示移除 - */ - fun setParticleStatus(index: Int, generated: Boolean) { - if (index !in 0 until count || isParticleDisplayed(index) == generated) return - setBit(this.index.get(), index, generated) - displayedParticleCount += if (generated) 1 else -1 - if (client) { - if (generated) createWithIndex(index) else removeWithIndex(index) - } - markDirty() - } - - fun isParticleDisplayed(index: Int): Boolean { - if (index !in 0 until count) return false - return getBit(this.index.get(), index) - } - - final override fun toggleScaleDisplayed() { - if (!displayed) { - return - } - sequencedParticlesData.forEach { (data, location) -> - applyScale(data.uuid, location) - } - toggleRelative() - } - - /** - * 客户端使用的索引到 UUID 映射,用于按索引删除。 - * 服务器不会读取该映射。 - */ - private var indexToUuid: Array = emptyArray() - private fun createWithIndex(i: Int) { - if (!client) return - if (i !in 0 until sequencedParticlesData.size) return - if (indexToUuid.size != count) indexToUuid = arrayOfNulls(count) - - // 如果已经创建过就跳过(避免重复) - if (indexToUuid[i] != null) return - - val (data, rl) = sequencedParticlesData[i] - displayEntry(data, rl) - if (particles.containsKey(data.uuid)) { - indexToUuid[i] = data.uuid - } - } - - override fun displayEntry(data: CompositionData, pos: RelativeLocation) { - super.displayEntry(data, pos) - refreshGpuTransformMode() - } - - override fun trackDisplayedParticleLocation(pos: RelativeLocation) = Unit - - - private fun removeWithIndex(i: Int) { - if (!client) return - if (i !in 0 until count) return - if (indexToUuid.size != count) return - - val uuid = indexToUuid[i] ?: return - val obj = particles[uuid] ?: run { - indexToUuid[i] = null - return - } - - unregisterCParticleNode(obj) - obj.remove() - if (obj is Tickable<*>) { - controlerTicks.remove(obj) - } - particles.remove(uuid) - particleLocations.remove(obj) - indexToUuid[i] = null - refreshGpuTransformMode() - } - - - private fun ensureIndexCapacity(newCount: Int) { - val pages = pagesFor(newCount) - val current = index.get() - if (current.size == pages) return - - val resized = LongArray(pages) - // 把旧页复制到新数组 - val len = min(current.size, resized.size) - for (i in 0 until len) resized[i] = current[i] - index.setMemoValue(resized) - } - - private fun pagesFor(cnt: Int): Int { - if (cnt <= 0) return 0 - return (cnt + 63) / 64 - } - - private fun getBit(arr: LongArray, index: Int): Boolean { - if (arr.isEmpty()) return false - val page = index ushr 6 // /64 - if (page !in arr.indices) return false - val bit = index and 63 - return ((arr[page] ushr bit) and 1L) == 1L - } - - private fun setBit(arr: LongArray, index: Int, value: Boolean) { - if (arr.isEmpty()) return - val page = index ushr 6 - if (page !in arr.indices) return - val bit = index and 63 - val mask = 1L shl bit - arr[page] = if (value) (arr[page] or mask) else (arr[page] and mask.inv()) - } - -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt deleted file mode 100644 index 69a2c31a..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/SequencedParticleShapeComposition.kt +++ /dev/null @@ -1,203 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.StatusHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.TreeMap -import java.util.UUID - -/** - * - 只用于客户端嵌套(不允许单独发包生成) - * - 继承 SequencedParticleComposition:提供 SortedMap 序列数据(按 CompositionData.order 排序) - */ -class SequencedParticleShapeComposition(uuid: UUID) : SequencedParticleComposition(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 - - var spawnAge = 0 - - /** - * 是否在reversed 到0的时候清理粒子 (防止残留影响观感) - */ - var reversedClean = true - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - private set - - fun loadScaleHelper(min: Double, max: Double, scalingTick: Int): SequencedParticleShapeComposition { - scaleHelper = CompositionScaleHelper(min, max, scalingTick).apply { - loadControler(this@SequencedParticleShapeComposition) - } - scalePreTick = true - return this - } - - fun loadScaleHelperBezierValue( - minScale: Double, - maxScale: Double, - scaleTick: Int, - c1: RelativeLocation, - c2: RelativeLocation - ): SequencedParticleShapeComposition { - scaleHelper = CompositionBezierScaleHelper(scaleTick, minScale, maxScale, c1, c2) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - /** - * 改用apply是因为 要防止和addPreTickAction冲突 - */ - fun applyDisplayAction(action: SequencedParticleShapeComposition.() -> Unit): SequencedParticleShapeComposition { - invokes.add(action) - return this - } - - fun applyBeforeDisplayAction( - action: SequencedParticleShapeComposition.(SortedMap) -> Unit - ): SequencedParticleShapeComposition { - beforeInvokes.add(action) - return this - } - - fun applyPoint( - point: RelativeLocation, - dataSupplier: (RelativeLocation, Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(PointsBuilder().addPoint(point) to dataSupplier) - return this - } - - fun applyPointRel( - point: RelativeLocation, - dataSupplier: (RelativeLocation) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(rel) }) - return this - } - - fun applyPointI( - point: RelativeLocation, - dataSupplier: (Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(o) }) - return this - } - - - fun applyBuilder( - builder: PointsBuilder, - dataSupplier: (RelativeLocation, Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(builder to dataSupplier) - return this - } - - fun applyBuilderI( - builder: PointsBuilder, - dataSupplier: (Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(builder to { rel, o -> dataSupplier(o) }) - return this - } - - fun applyBuilderRel( - builder: PointsBuilder, - dataSupplier: (RelativeLocation) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(builder to { rel, o -> dataSupplier(rel) }) - return this - } - - fun setReversedScaleOnDisableStatus(status: StatusHelper): SequencedParticleShapeComposition { - addPreTickAction { - if (status.displayStatus == 2) { - scaleReversed = true - } - } - return this - } - - fun setReversedScaleOnCompositionStatus(composition: ParticleComposition): SequencedParticleShapeComposition { - addPreTickAction { - if (composition.status.getCurrentStatus() == StatusHelper.Status.DISABLE) { - scaleReversed = true - } - } - return this - } - - override fun getCodec(): StreamCodec { - throw NotImplementedError("此类只作为客户端嵌套使用, 不能单独生成! ") - } - - /** - * SequencedParticleComposition 需要返回 SortedMap - * TreeMap 会按 CompositionData.compareTo -> order 排序 - */ - override fun getParticleSequenced(): SortedMap { - val res: SortedMap = TreeMap() - var order = 0 - points.forEach { (builder, supplier) -> - res.putAll( - builder.createWithCompositionData { rel -> - supplier(rel, order++) - } - ) - } - return res - } - - - override fun beforeDisplaySequenced(map: SortedMap) { - super.beforeDisplaySequenced(map) - beforeInvokes.forEach { - it(map) - } - } - - override fun onDisplay() { - animate.clientOnly() - 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt deleted file mode 100644 index bafbcb49..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/manager/ParticleCompositionManager.kt +++ /dev/null @@ -1,379 +0,0 @@ -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.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -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 { - /** - * 弱引用保存已进入客户端显示生命周期的 Composition 实例。 - * - * 示例:顶层 Composition 及其直接显示的子 Composition 会分别占一个条目。 - * 禁止在此处强持有 Composition,否则 Emitter 临时创建的实例无法被回收。 - */ - private val loadedClientCompositions = WeakIdentitySet() - - val clientView = ConcurrentHashMap() - - val serverView = ConcurrentHashMap() - - /** - * 只有玩家可见才会处理发包 - */ - val playerPlayerVisibleSet = ConcurrentHashMap>() - - val registeredTypes = ConcurrentHashMap>() - - /** - * 登记或移除一个客户端活动 Composition。 - * - * 示例:Composition 首次 `display()` 时传入 `true`,执行 `clear(true)` 时传入 `false`。 - * 禁止为服务端仅用于同步的 Composition 传入 `true`。 - * - * @param composition 需要更新活动状态的 Composition 实例 - * @param loaded `true` 表示已加载到客户端,`false` 表示已离开客户端生命周期 - */ - internal fun setClientLoaded(composition: ParticleComposition, loaded: Boolean) { - if (loaded) { - loadedClientCompositions.add(composition) - } else { - loadedClientCompositions.remove(composition) - } - } - - /** - * 返回客户端当前活动的全部 Composition 实例数量。 - * - * 示例:F3 调试信息用该值统计顶层、嵌套及 Emitter 直接显示的 Composition。 - * 禁止把该值理解为服务端实例数或已注册类型数。 - * - * @return 当前客户端活动 Composition 数量 - */ - @JvmStatic - fun loadedClientCount(): Int = loadedClientCompositions.size() - - /** 返回当前客户端活动的全部 Composition 快照,包含嵌套实例。 */ - internal fun debugCompositions(): List = loadedClientCompositions.snapshot() - - /** 返回服务端当前活动 Composition 实例数。 */ - 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 = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) - 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) - } - } - } - - /** - * 清理当前客户端世界持有的所有 Composition。 - * - * 示例:客户端断线或换世界时调用该方法,将顶层与嵌套实例一并移出活动计数。 - * 禁止用该方法清理服务端 [serverView]。 - */ - fun clearClient() { - // 这里必须走 clear(true) 强制销毁 - // remove() 可能被使用者重写成延迟消散的语义 (例如先 status.disable() 等渐隐结束再真正销毁) - // 客户端断连/换世界要求立刻干净 否则 composition 会脱离 clientView 变成孤儿粒子 - clientView.values.forEach { - it.clear(true) - } - clientView.clear() - loadedClientCompositions.clear() - } - - - fun clearServer() { - // 同 clearClient 不能依赖使用者重写的 remove() - 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt deleted file mode 100644 index 20e2b631..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/DoubleRangeData.kt +++ /dev/null @@ -1,26 +0,0 @@ -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) -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt deleted file mode 100644 index acb79b08..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/FloatRangeData.kt +++ /dev/null @@ -1,19 +0,0 @@ -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) -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt deleted file mode 100644 index 2fb0b504..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/IntRangeData.kt +++ /dev/null @@ -1,24 +0,0 @@ -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) -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt deleted file mode 100644 index e342b4b6..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/data/RangeData.kt +++ /dev/null @@ -1,7 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.data - -abstract class RangeData>(var min: T, var max: T) { - init { - require(min <= max) { "min must be <= $max" } - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt deleted file mode 100644 index 2c83b326..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoEmitters.kt +++ /dev/null @@ -1,29 +0,0 @@ -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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * 自动生成网络 codec 并以实现类完整类名注册的 [ClassEmitters]。 - * - * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; - * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, - * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 - * - * @param pos 发射器生成位置 - * @param world 发射器生效世界 - */ -abstract class AutoEmitters(pos: Vec3, world: Level?) : ClassEmitters(pos, world) { - override fun getEmittersID(): String { - return this::class.java.name - } - - override fun getCodec(): StreamCodec { - return ParticleEmittersRegistryHelper.generateCodec(this) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt deleted file mode 100644 index 2491381d..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoParticleEmitters.kt +++ /dev/null @@ -1,32 +0,0 @@ -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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * 自动生成网络 codec 并以实现类完整类名注册的 [ClassParticleEmitters]。 - * - * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; - * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, - * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 - * - * @see ParticleEmittersRegistryHelper - * @constructor 实现类必须提供空构造方法或 `(Vec3, Level?)` 构造方法 - * - * @param pos 发射器生成位置 - * @param world 发射器生效世界 - */ -abstract class AutoParticleEmitters(pos: Vec3, world: Level?) : ClassParticleEmitters(pos, world) { - override fun getEmittersID(): String { - return this::class.java.name - } - - override fun getCodec(): StreamCodec { - return ParticleEmittersRegistryHelper.generateCodec(this) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt deleted file mode 100644 index b0f80bb3..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/AutoTransformableCParticleEmitter.kt +++ /dev/null @@ -1,29 +0,0 @@ -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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * 自动生成网络 codec 并以实现类完整类名注册的 [TransformableCParticleEmitter]。 - * - * 实现类必须标注 [CooAutoRegister],并提供公开的 `(Vec3, Level?)` 构造器。 - * 普通同步字段可以使用 [CodecField];会在运行时修改的 Kotlin 属性建议使用 - * `var value by dirty(initialValue)`,赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 - * 基类生命周期和变换字段会自动编码。 - * - * @param pos 发射器世界坐标 - * @param world 发射器所在世界 - */ -abstract class AutoTransformableCParticleEmitter(pos: Vec3, world: Level?) : - TransformableCParticleEmitter(pos, world) { - final override fun getEmittersID(): String = this::class.java.name - - final override fun getCodec(): StreamCodec { - return ParticleEmittersRegistryHelper.generateCodec(this) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt deleted file mode 100644 index 01992fcc..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -/** - * 定义 [TransformableCParticleEmitter] 新生成粒子使用的坐标空间。 - * - * [LOCAL] 把粒子位置和速度保存在发射器局部空间中。粒子生成后仍会跟随发射器平移、 - * [TransformableCParticleEmitter.emitterRotation] 和缩放, - * 也是默认模式。[WORLD] 使用普通 GPU 发射器的世界坐标语义,已经生成的粒子不再受发射器变换影响。 - * 模式切换只影响后续生成的粒子,已有粒子继续留在各自创建时的空间中。 - * - * @property networkId 稳定的网络编号,不依赖枚举声明顺序 - */ -enum class CParticleEmitterSpace(val networkId: Int) { - /** 粒子属于发射器局部空间,并持续跟随发射器变换。 */ - LOCAL(0), - - /** 粒子生成后保留在世界空间,不跟随发射器变换。 */ - WORLD(1); - - companion object { - /** - * 按稳定网络编号恢复发射空间。 - * - * @param networkId 网络数据中的空间编号 - * @return 对应的发射空间 - * @throws IllegalArgumentException 编号未知时抛出 - */ - @JvmStatic - fun fromNetworkId(networkId: Int): CParticleEmitterSpace { - return entries.firstOrNull { it.networkId == networkId } - ?: throw IllegalArgumentException("unknown CParticle emitter space: $networkId") - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt deleted file mode 100644 index b018ccde..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassEmitters.kt +++ /dev/null @@ -1,411 +0,0 @@ -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.FriendlyByteBuf -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 - -/** - * 通用发射器实现,兼容多种可控对象: - * - [ControlableParticleData] - * - [ParticleComposition] - * - [DisplayEntity] - */ -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>() - - /** - * 是否启用线段插值器。 - * - * 如果设置为 `true`,每次发射会沿插值结果逐点生成;设置为 `false` 时,每 tick 只执行一次生成。 - */ - var enableInterpolator = false - - /** - * 发射器位移插值器。 - * - * 仅在 [enableInterpolator] 为 `true` 时生效,可通过 `setRefiner` 调整细分密度。 - */ - 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: FriendlyByteBuf) { - 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) - } - - /** 写法:先在 codec 的 decode 中创建对象,再把 `buf` 和 `container` 传入此方法,最后继续解码子类字段。 */ - fun decodeBase(container: ClassEmitters, buf: FriendlyByteBuf) { - 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) - } - - /** 质量,单位 g。 */ - 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 - } - - /** - * 服务器和客户端都会执行此方法。 - * - * 如需仅在服务端执行,请自行判断: - * ```kotlin - * if (!world!!.isClientSide) { ... } - * ``` - */ - abstract fun doTick() - - /** - * 可控对象生成器。 - * - * @param lerpProgress 发射器位移插值器的插值进度 - */ - abstract fun genControls(lerpProgress: Float): List> - - /** - * 在一次生成前会执行。 - * - * @param current 当前插值点的位置 - * @param lerpProgress 当前生成进度 - */ - protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - - /** - * 通用可控对象的生成后回调。 - * - * @param data 当前生成对象的数据 - * @param spawnPos 生成位置(对象创建后再改该值无效) - * @param particleLerpProgress 当前对象在本次批量生成中的进度 - * @param posLerpProgress 发射器位移插值进度(未启用插值时恒为 1) - */ - abstract fun singleControlableAction( - controler: Controlable<*>, - data: SerializableData, - spawnPos: RelativeLocation, - spawnWorld: Level, - particleLerpProgress: Float, - posLerpProgress: Float, - ) - - /** - * 处理单个 controler 的移动方案 - */ - protected open fun moveSingleControler( - controler: Controlable<*>, - data: SerializableData, - to: Vec3, - collide: BlockHitResult - ) { - controler.teleportTo(to) - } - - /** - * 读取当前 controler 世界坐标。 - * - * 子类可重写以支持自定义可控对象,默认支持: - * - [ControlableParticle] - * - [ParticleComposition] - * - [DisplayEntity] - */ - 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 - } - } - - /** - * 读取当前 controler 的速度向量。 - * - * 子类可重写以支持自定义数据类型;默认对 [ControlableParticleData] 生效。 - */ - 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) - } - } - - - /** - * 数据同步需要实现此方法。 - * - * @param emitters 需要同步到当前实例的发射器模板 - */ - 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt deleted file mode 100644 index a9fe79a5..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ClassParticleEmitters.kt +++ /dev/null @@ -1,594 +0,0 @@ -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.FriendlyByteBuf -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>() - - /** - * 是否启用线段插值器 如果设置为true 则spawnParticles由线段插值器管控 设置为false,则只会在 - * 1tick内执行一次spawnParticles - */ - var enableInterpolator = false - - /** - * 插值器工具 需要启用 enableInterpolator 可以修改 - * - * @see enableInterpolator - */ - var emittersInterpolator: Interpolator = LineEmitterInterpolator() - .setRefiner(5.0) - - override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { - val handlerID = handler.getHandlerID() - // 自动注册不适用于多人 - // TODO - 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 { - // 保留innerClass为false的 - it.value.filter { it -> - !it.value - }.keys - } - } - - - companion object { - fun encodeBase(data: ClassParticleEmitters, buf: FriendlyByteBuf) { - 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) - } - - /** 写法 先在codec的 decode方法中 创建此对象 然后将buf和container 传入此方法 然后继续decode自己的参数 */ - fun decodeBase(container: ClassParticleEmitters, buf: FriendlyByteBuf) { - 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) - } - - /** - * 旧版 GPU 力场列表接口。 - * - * 旧 emitter 可以继续覆写此方法;桥接层会在每 tick 调用 - * [submitCParticleForces],由默认实现把列表内容提交到 sink。 - * 新 emitter 应直接覆写流式接口,避免创建临时列表。 - * - * @return 按执行顺序排列的力场列表 - */ - open fun cparticleForces(): List = emptyList() - - /** - * GPU 模式附加力场 (每 tick 同步一次)。 - * - * 默认实现兼容旧版 [cparticleForces];新 emitter 可以直接向 sink 提交, - * 从而避免每 tick 构建临时列表。内置 ParticleCommand 可用 - * [CParticleForce.fromCommand] 转换。 - * - * @param sink 当前 emitter 的力场快照,调用期间只应提交本 emitter 的力 - */ - open fun submitCParticleForces(sink: CParticleForceSink) { - sink.submitAll(cparticleForces()) - } - - /** - * CParticle 方块碰撞相对 system 原点的最大保证范围,单位为方块。 - * - * 示例:粒子最远会离开 system 原点 40 格时返回 `40`。 - * 禁止:返回值会被限制在 `0..96`;范围越大,网格刷新成本按立方增长。 - */ - open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE - - /** 质量 单位 g */ - 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, - ) - } - } - - /** - * 服务器和客户端都会执行此方法 判断服务器清使用 - * - * ```kotlin - * if(!world!!.isClient) - * ``` - */ - abstract fun doTick() - - /** - * 粒子样式生成器 - * @param lerpProgress 粒子发射器位移插值器的插值进度 - * */ - abstract fun genParticles(lerpProgress: Float): List> - - /** - * 在一次粒子生成前会执行 - * - * @param current 当前插值的生成位置 - * @param lerpProgress 粒子生成的进度 - */ - protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - - /** - * 如若要修改粒子的位置, 速度 属性 请直接修改 ControlableParticleData - * - * @param data 用于操作单个粒子属性的类 - * @param spawnPos 生成的位置,在粒子已经生成后再修改无效 - * 执行tick方法请使用controler.addPreTickAction - * @param particleLerpProgress - * 生成这个粒子的时候,当前的进度((当前生成的粒子索引+1)/genParticles(lerpProgress: Float).size ) - * @param posLerpProgress 当发射器进行发射插值时, 插值的偏移 如果不使用插值则永远为1 - */ - abstract fun singleParticleAction( - controler: ParticleControler, - data: ControlableParticleData, - spawnPos: RelativeLocation, - spawnWorld: Level, - particleLerpProgress: Float, - posLerpProgress: Float, - ) - - /** - * 当粒子死亡(age >= maxAge然后remove)时 会执行这个方法 - * - * @param oldControler - * @param oldData - * @param respawnCount 重新生成的次数 (0代表第一次从emitter生成) - * @param reason 粒子移除原因 - * @return 会根据data生成新粒子 第二个pair是和粒子死亡位置的相对位置(不是发射器相对位置) - */ - 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 - } - // cparticle GPU 路径: 数据直接进 GPU 粒子系统, 跳过 controler/事件/碰撞 - 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 { - // 针对 ParticleHitEntityEvent - 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 { - // 针对 ParticleOnLiquidEvent - 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 - // 判断现在的位置是不是液体 - if (!world.shouldTickBlocksAt(blockPos)) { - return@addPreTickAction - } - val state = world.getBlockState(blockPos) - val currentLiquid = !state.isSolid - control.bufferedData["cross_liquid"] = currentLiquid - if (beforeLiquid || !currentLiquid) { - return@addPreTickAction - } - // 前一个tick不是液体 当前tick是液体则触发事件 - 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) { - // 找方向 velocity - handlerList[ParticleOnGroundEvent.EVENT_ID]?.let { - val offset = clipRes.direction.normal.asVec3() * 0.1 - val event = ParticleOnGroundEvent( - this, - data, - ofFloored(prepareMove), - clipRes.location.add(offset), - clipRes - ) - for ((handler, _) in it) { - if (handler.getTargetEventID() != ParticleOnGroundEvent.EVENT_ID) { - continue - } - handler.handle(event) - if (event.canceled) { - break - } - } - } - } - - if (clipRes.type != HitResult.Type.MISS) { - handlerList[ParticleCollideEvent.EVENT_ID]?.let { - val event = ParticleCollideEvent( - this, data, clipRes - ) - for ((handler, _) in it) { - 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) - } - - - /** - * # 处理单个粒子的位移位置 - * - 方便更真实的物理模拟 - * - 用于修改particle受到velocity时的移动 - * - * @param particle 被操作位置的粒子 - * @param data 被操作位置的粒子数据 - * @param to 正常情况下粒子应该移动到的位置(单纯loc+velocity) - * @param collide 粒子的碰撞情况 (如果显示碰撞,则代表to位置存在方块 loc不存在) - */ - protected open fun moveSingleParticleWithVelocity( - particle: ControlableParticle, - data: ControlableParticleData, - to: Vec3, - collide: BlockHitResult - ) { - particle.teleportTo(to) - } - - /** - * 数据同步需要实现此方法 - * - * @param emitters 更新的模板发射器 - */ - 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt deleted file mode 100644 index 577f7b1e..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CompositionEmittersData.kt +++ /dev/null @@ -1,117 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -import cn.coostack.cooparticlesapi.api.controler.Controlable -import cn.coostack.cooparticlesapi.api.controler.SerializableData -import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import io.netty.buffer.Unpooled -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * SerializableData wrapper for ParticleComposition emitters entry. - */ -class CompositionEmittersData( - var compositionType: String = "", - var compositionData: ByteArray = ByteArray(0) -) : SerializableData { - constructor(composition: ParticleComposition) : this() { - setComposition(composition) - } - - companion object { - @JvmStatic - val PACKET_CODEC: StreamCodec = - StreamCodec.of( - { buf, data -> - buf.writeUtf(data.compositionType) - buf.writeInt(data.compositionData.size) - buf.writeBytes(data.compositionData) - }, - { buf -> - val type = buf.readUtf() - val size = buf.readInt() - val bytes = ByteArray(size) - buf.readBytes(bytes) - CompositionEmittersData(type, bytes) - } - ) - - @JvmStatic - fun fromComposition(composition: ParticleComposition): CompositionEmittersData { - val buf = FriendlyByteBuf(Unpooled.buffer()) - composition.getCodec().encode(buf, composition) - val data = ByteArray(buf.readableBytes()) - buf.readBytes(data) - return CompositionEmittersData(composition::class.java.name, data) - } - } - - private var prepared: ParticleComposition? = null - - private fun decodeComposition(): ParticleComposition { - val codec = ParticleCompositionManager.registeredTypes[compositionType] - ?: throw IllegalStateException("ParticleComposition codec not registered for type: $compositionType") - return codec.decode(FriendlyByteBuf(Unpooled.wrappedBuffer(compositionData.copyOf()))) - } - - private fun cloneComposition(composition: ParticleComposition): ParticleComposition { - val type = composition::class.java - val ins = runCatching { - type.getDeclaredConstructor(Vec3::class.java, Level::class.java) - .apply { isAccessible = true } - .newInstance(composition.position, composition.world) - }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() - - return ins.apply { update(composition) } - } - - private fun resolveComposition(): ParticleComposition { - return prepared ?: decodeComposition().also { prepared = it } - } - - override fun getCodec(): StreamCodec { - return PACKET_CODEC - } - - override fun clone(): SerializableData { - val composition = runCatching { cloneComposition(resolveComposition()) }.getOrNull() - return if (composition != null) { - CompositionEmittersData(composition) - } else { - CompositionEmittersData(compositionType, compositionData.copyOf()) - } - } - - override fun createControler( - world: ClientLevel, - pos: Vec3, - particleLerpProcess: Float, - posLerpProcess: Float - ): Controlable<*> { - val composition = resolveComposition() - composition.world = world - composition.setPositionWithoutToggle(pos) - prepared = composition - return composition - } - - override fun getDisplayer(): ParticleDisplayer { - val composition = resolveComposition() - return ParticleDisplayer.withComposition(composition) - } - - fun setComposition(composition: ParticleComposition): CompositionEmittersData { - val clone = cloneComposition(composition) - val data = fromComposition(clone) - compositionType = data.compositionType - compositionData = data.compositionData - prepared = clone - return this - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt deleted file mode 100644 index 4d04092c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableCParticleData.kt +++ /dev/null @@ -1,266 +0,0 @@ -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 net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import org.joml.Vector3f - -/** - * 单个 CParticle GPU 粒子的完整生成数据。 - * - * 额外纹理蒙版、生命周期曲线、旋转和 GPU 更新方式都属于每份 data,且本类型只让当前 data - * 进入 CParticle 路径。同一次 - * `genParticles()` 可以混合本类型与普通 [ControlableParticleData]:前者走 GPU,后者继续执行 - * `singleParticleAction` 和 [sign] 对应的 CPU 行为。基础纹理始终来自父类 [effect], - * [textureSource] 只控制叠加在基础纹理上的可选蒙版。 - * - * Example: - * ```kotlin - * val block = ControlableCParticleData().apply { - * textureSource = textureOfBlock(state) - * } - * val item = ControlableCParticleData().apply { - * textureSource = textureOfItem(stack) - * } - * ``` - * Forbidden: 不要把一次 emitter 调用中的公共纹理写回 emitter 单例。 - */ -open class ControlableCParticleData : ControlableParticleData() { - /** - * # STATIC - * 生成后不保留本 data; - * - * # DYNAMIC - * 允许同步可变外观 - * - * 不建议使用DYNAMIC,因为GPU粒子不支持singleAction, - * 这会导致你必须得额外存储每一个粒子的Data,并且按照对应的方式自己修改 - * 非常浪费性能的同时,也不是这个API希望的做法, - * 颜色、生命周期透明度和缩放都已有 STATIC 曲线,不需要为这类变化使用 DYNAMIC。 - * - * 按照GPU粒子的设定一般使用STATIC - * */ - var updateMode: CParticleUpdateMode = CParticleUpdateMode.STATIC - - /** - * 叠加在 [effect] 基础纹理上的可选蒙版;`null` 表示不叠加蒙版。 - * - * Example: 同一 emitter 的不同 CParticle data 可以叠加不同方块或物品纹理。 - * Forbidden: 生成后的 STATIC 粒子不会继续引用或观察此对象。 - */ - var textureSource: CParticleTextureSource? = null - - /** 按 `age / maxAge` 在 GPU 中采样的不透明度乘数曲线。 */ - var alphaCurve: CParticleCurve? = null - - /** - * 按 `age / maxAge` 同时缩放 X/Y 尺寸的等比 GPU 曲线。 - * - * 最终会与 [scaleXCurve]、[scaleYCurve] 的对应轴倍率相乘。 - * Example: `scaleCurve = CParticleCurve.fadeInOut()` 会保持当前宽高比例。 - * Forbidden: 不要用它单独控制某一个轴。 - */ - var scaleCurve: CParticleCurve? = null - - /** - * 按 `age / maxAge` 只缩放 X 方向尺寸的 GPU 曲线。 - * - * 示例:从 `0.2F` 变化到 `1F` 可以让粒子横向展开。 - * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 - */ - var scaleXCurve: CParticleCurve? = null - - /** - * 按 `age / maxAge` 只缩放 Y 方向尺寸的 GPU 曲线。 - * - * 示例:从 `1F` 变化到 `0F` 可以让粒子纵向收拢。 - * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 - */ - var scaleYCurve: CParticleCurve? = null - - /** 按 `age / maxAge` 与基础颜色相乘的 GPU RGB 曲线。 */ - var colorCurve: CParticleColorCurve? = null - - /** ROTATION 模式的指向向量;`null` 时使用父类 yaw/pitch。 */ - var rotationDirection: Vector3f? = null - - /** 欧拉角速度,单位 rad/tick;x=pitch、y=yaw、z=roll。 */ - var angularVelocity: Vector3f = Vector3f() - - /** 每个整数 tick 使用稳定随机种子重新选择纹理动画帧。 */ - var randomAgePreTick: Boolean = false - - /** GPU 随机帧和随机 UV 的种子;`null` 表示生成时自动分配。 */ - var randomSeed: Int? = null - - /** - * 是否使用 CParticle 共享方块占用网格处理本粒子的位移碰撞。 - * - * 碰撞响应与 `moveSingleParticleWithVelocity` 的常用实现一致:命中后停在表面前, - * 并移除速度的法线分量。复杂 `VoxelShape` 会按完整方块近似。 - * 共享缓存覆盖 system 原点附近的 `64³` 方块,分桶边缘至少保留 24 格;缓存外按未命中处理。 - * Example: 同一次 `genParticles()` 可以只给落尘 data 设置为 `true`。 - * Forbidden: 不要用它代替需要台阶、栅栏或实体精确形状的 CPU 碰撞。 - */ - var blockCollision: Boolean = false - - /** 供 Force Command 的 CommandMask 选择。 */ - var commandMask: Int = 0 - - /** metadata 扩展标志位。 */ - var metadataFlags: Int = 0 - - /** 粒子电荷;NaN 表示由 Charge command 使用默认值。 */ - var charge: Float = Float.NaN - - /** 粒子半径,供单点 Lennard-Jones 使用。 */ - var radius: Float = 0F - - /** - * 保存专用 data 的网络编解码器。 - * - * Example: `@CodecField var template = ControlableCParticleData()` 会通过此 codec 同步。 - * Forbidden: 不要用父类 codec 解码本类型,否则 [textureSource] 会丢失。 - */ - companion object { - /** - * 先处理继承的基础字段,再处理可空纹理来源。 - * - * Example: 编解码后 `sign`、velocity、effect 和 textureSource 都保持不变。 - * Forbidden: 字段顺序不能与 [decode] 分离修改。 - */ - @JvmField - val PACKET_CODEC: StreamCodec = - StreamCodec.of(::encode, ::decode) - - /** - * 编码基础字段和当前 data 的可选纹理来源。 - * - * Example: `null` 只写入一个 false,客户端继续使用 effect。 - * Forbidden: 不要在这里解析模型或 UV。 - * - * @param buf 目标网络缓冲区 - * @param data 要编码的 CParticle data - */ - private fun encode(buf: RegistryFriendlyByteBuf, data: ControlableCParticleData) { - encodeBase(buf, data) - val source = data.textureSource - buf.writeBoolean(source != null) - if (source != null) { - CParticleTextureSource.STREAM_CODEC.encode(buf, source) - } - buf.writeByte(data.updateMode.ordinal) - writeNullable(buf, data.alphaCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.scaleCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.scaleXCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.scaleYCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.colorCurve, CParticleColorCurve.STREAM_CODEC) - val direction = data.rotationDirection - buf.writeBoolean(direction != null) - if (direction != null) buf.writeVector3f(direction) - buf.writeVector3f(data.angularVelocity) - buf.writeBoolean(data.randomAgePreTick) - val seed = data.randomSeed - buf.writeBoolean(seed != null) - if (seed != null) buf.writeInt(seed) - buf.writeBoolean(data.blockCollision) - buf.writeInt(data.commandMask) - buf.writeInt(data.metadataFlags) - buf.writeFloat(data.charge) - buf.writeFloat(data.radius) - } - - /** - * 解码基础字段,并在同一个子类实例上恢复纹理来源。 - * - * Example: Item 来源解码后仍持有独立的 ItemStack 快照。 - * Forbidden: 不能先通过父类 codec 解码后再强制转换。 - * - * @param buf 来源网络缓冲区 - * @return 完整的 CParticle data - */ - private fun decode(buf: RegistryFriendlyByteBuf): ControlableCParticleData { - val data = decodeBase(buf, ControlableCParticleData()) - if (buf.readBoolean()) { - data.textureSource = CParticleTextureSource.STREAM_CODEC.decode(buf) - } - val updateModeOrdinal = buf.readUnsignedByte().toInt() - require(updateModeOrdinal < CParticleUpdateMode.entries.size) { - "unknown CParticle update mode: $updateModeOrdinal" - } - data.updateMode = CParticleUpdateMode.entries[updateModeOrdinal] - data.alphaCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.scaleCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.scaleXCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.scaleYCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.colorCurve = readNullable(buf, CParticleColorCurve.STREAM_CODEC) - if (buf.readBoolean()) data.rotationDirection = buf.readVector3f() - data.angularVelocity = buf.readVector3f() - data.randomAgePreTick = buf.readBoolean() - if (buf.readBoolean()) data.randomSeed = buf.readInt() - data.blockCollision = buf.readBoolean() - data.commandMask = buf.readInt() - data.metadataFlags = buf.readInt() - data.charge = buf.readFloat() - data.radius = buf.readFloat() - return data - } - - private fun writeNullable( - buf: RegistryFriendlyByteBuf, - value: T?, - codec: StreamCodec, - ) { - buf.writeBoolean(value != null) - if (value != null) codec.encode(buf, value) - } - - private fun readNullable( - buf: RegistryFriendlyByteBuf, - codec: StreamCodec, - ): T? = if (buf.readBoolean()) codec.decode(buf) else null - } - - /** - * 返回包含纹理来源的专用 codec。 - * - * Example: emitter 自动编码器可从实例查询此 codec。 - * Forbidden: 返回父类 codec 会截断扩展字段。 - * - * @return [PACKET_CODEC] - */ - override fun getCodec(): StreamCodec = PACKET_CODEC - - /** - * 复制基础字段和当前纹理来源,返回独立的 [ControlableCParticleData]。 - * - * Example: emitter 模板可以为每个生成结果调用 `clone()`。 - * Forbidden: 派生类若有额外字段,必须覆盖本方法,不能依赖本实现保留派生类型。 - * - * @return 带有新 UUID 的 CParticle data - */ - override fun clone(): ControlableCParticleData { - return ControlableCParticleData().also { - copyTo(it) - it.updateMode = updateMode - it.textureSource = textureSource - it.alphaCurve = alphaCurve - it.scaleCurve = scaleCurve - it.scaleXCurve = scaleXCurve - it.scaleYCurve = scaleYCurve - it.colorCurve = colorCurve - it.rotationDirection = rotationDirection?.let(::Vector3f) - it.angularVelocity = Vector3f(angularVelocity) - it.randomAgePreTick = randomAgePreTick - it.randomSeed = randomSeed - it.blockCollision = blockCollision - it.commandMask = commandMask - it.metadataFlags = metadataFlags - it.charge = charge - it.radius = radius - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt deleted file mode 100644 index 522c1dd1..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ControlableParticleData.kt +++ /dev/null @@ -1,458 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -import cn.coostack.cooparticlesapi.CooParticlesConstants -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.ParticleTypes -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -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: StreamCodec = - StreamCodec.of( - ::encodeBase, - { buf -> decodeBase(buf, ControlableParticleData()) }, - ) - - /** - * 编码所有基础粒子字段,供父类和扩展 data 共用。 - * - * Example: `ControlableCParticleData` 先调用此方法,再写入自己的纹理来源。 - * Forbidden: 子类不能改变这些字段的顺序,否则旧的基础 data 无法解码。 - * - * @param buf 目标网络缓冲区 - * @param data 要编码的基础粒子数据 - */ - internal fun encodeBase(buf: RegistryFriendlyByteBuf, 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) - - ParticleTypes.STREAM_CODEC.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) - } - - /** - * 从缓冲区读取基础字段并写入指定实例。 - * - * Example: 子类传入自己的新实例,解码后仍保留实际运行时类型。 - * Forbidden: 不要在这里固定构造 `ControlableParticleData`,否则子类字段会丢失。 - * - * @param buf 来源网络缓冲区 - * @param target 接收基础字段的实例 - * @return 赋值完成的 [target] - */ - internal fun decodeBase( - buf: RegistryFriendlyByteBuf, - target: T, - ): T { - val uuid = buf.readUUID() - val velocity = buf.readVec3() - val weightSize = buf.readFloat() - val heightSize = buf.readFloat() - val uniformSize = buf.readBoolean() - val visibleRange = buf.readFloat() - val color = buf.readVector3f() - val alpha = buf.readFloat() - val age = buf.readInt() - val maxAge = buf.readInt() - val textureSheet = buf.readUtf() - val effect = ParticleTypes.STREAM_CODEC.decode(buf) as ControlableParticleEffect - effect.controlUUID = uuid - val speed = buf.readDouble() - val speedLimit = buf.readDouble() - val sign = buf.readInt() - val light = buf.readInt() - val cameraOption = ParticleCameraOption.STREAM_CODEC.decode(buf) - val axis = buf.readVec3() - val yaw = buf.readFloat() - val pitch = buf.readFloat() - val roll = buf.readFloat() - val depthSize = buf.readFloat() - return target.apply { - this.uuid = uuid - this.velocity = velocity - this.color = color - this.alpha = alpha - this.uniformSize = uniformSize - this.weightSize = weightSize - this.heightSize = heightSize - this.depthSize = depthSize - this.visibleRange = visibleRange - this.age = age - this.maxAge = maxAge - this.textureSheet = textureSheet - this.effect = effect - this.speed = speed - this.sign = sign - this.speedLimit = speedLimit - this.light = light - this.cameraOption = cameraOption - this.axis = axis - this.yaw = yaw - this.pitch = pitch - this.roll = roll - } - } - - @JvmStatic - fun registerRenderType(type: ParticleRenderType) { - particleTexturesMapper[type.toString()] = type - } - } - - - /** - * 粒子生成时会传输的控制UUID - */ - var uuid = UUID.randomUUID() - - /** - * 粒子的移动向量 - * 在粒子发射器中 会不断调用这次的参数 - * - * 此选项会一直赋值给实际粒子 - */ - var velocity: Vec3 = Vec3.ZERO - - /** - * 生成的粒子相机朝向模式,默认始终面向摄像头。 - */ - var cameraOption: ParticleCameraOption = ParticleCameraOption.BILLBOARD - - /** 旧布尔 API 的兼容桥接:true = BILLBOARD,false = ROTATION。 */ - var faceToCamera: Boolean - get() = cameraOption == ParticleCameraOption.BILLBOARD - set(value) { - cameraOption = ParticleCameraOption.fromFaceToCamera(value) - } - - /** AXIS_BILLBOARD 使用的固定轴方向。 */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 如果faceToCamera为false - * 则此参数代表了粒子水平朝向 - * - * 弧度制 - */ - var yaw = 0.0f - - /** - * 如果faceToCamera为false - * 则此参数代表了粒子垂直朝向 - * - * 弧度制 - * - */ - var pitch = 0.0f - - /** - * 此参数代表了粒子滚动朝向 - * - * 弧度制 - * - */ - var roll = 0.0f - - /** 是否保持宽高等比。 */ - var uniformSize = true - - /** 粒子宽度大小。 */ - private var currentWeightSize = 0.2f - var weightSize: Float - get() = currentWeightSize - set(value) { - currentWeightSize = value - if (uniformSize) { - currentHeightSize = value - } - } - - /** 粒子高度大小。 */ - private var currentHeightSize = 0.2f - var heightSize: Float - get() = currentHeightSize - set(value) { - currentHeightSize = value - if (uniformSize) { - currentWeightSize = value - } - } - - private var currentDepthSize = 0f - var depthSize: Float - get() = currentDepthSize - set(value) { - currentDepthSize = value - } - - /** 旧 size 兼容属性:设置时同时修改宽高。 */ - var size: Float - get() = (weightSize + heightSize) / 2f - set(value) { - currentWeightSize = value - currentHeightSize = value - } - - /** - * 粒子生成时采用的不透明度 - * - */ - var alpha = 1f - - /** - * 粒子生成时设置的age - * - */ - var age = 0 - - /** - * 粒子最大生命周期 - * - */ - var maxAge = 120 - - /** - * 粒子生成时的亮度 - * (修改粒子亮度时请修改该数据) - * - */ - var light = 15 - - /** - * 粒子可见范围 - */ - var visibleRange = 256f - - /** - * 粒子样式 (必须是可控制的粒子) - * - * 此选项只生效一次 - */ - var effect: ControlableParticleEffect = ControlableEndRodEffect(uuid) - - /** - * 一些特殊标识 - * 用于在single 区分不同类型 分工的粒子 - */ - var sign = 0 - - /** - * 粒子移动速度上限 - * 防止不知道什么原因导致粒子移速过高从而导致客户端卡死 - */ - var speedLimit = 32.0 - - /** - * 粒子生成时采用的颜色,后续控制不生效 - * - * 此选项只应用一次 - */ - var color = Vector3f(1f, 1f, 1f) - - /** - * 粒子重生次数 - * 他会在不断的粒子重生中递增 - */ - var respawnCount = 0 - internal set - - // 脑瘫东西设置了客户端专属 - // 粒子渲染方式 只生效一次 - private var textureSheet: String = "PARTICLE_SHEET_TRANSLUCENT" - - /** - * 粒子移动速度 - * 在ClassParticlesEmitters默认不生效 - */ - var speed: Double = 1.0 - fun textureSheetFromString(sheet: String): ParticleRenderType? { - return particleTexturesMapper[sheet] - } - - /** - * 快速旋转 yaw pitch 到目标点 - * - * @param to 目标相对位置 - */ - fun setRotationTo(to: Vector3f) { - val (x, y, z) = Math3DUtil.calculateEulerAnglesToPoint(to) - this.yaw = y - this.pitch = x - } - - /** - * 快速旋转 yaw pitch 到目标点 - * - * @param to 目标相对位置 - */ - fun setRotationTo(to: Vec3) { - setRotationTo(to.toVector3f()) - } - - /** - * 快速旋转 yaw pitch 到目标点 - * - * @param to 目标相对位置 - */ - fun setRotationTo(to: RelativeLocation) { - setRotationTo(to.toVector3f()) - } - - fun setAxisLocation(axis: RelativeLocation) { - this.axis = Vec3(axis.x, axis.y, axis.z) - } - - fun getTextureSheet(): ParticleRenderType { - return textureSheetFromString(textureSheet) ?: let { - CooParticlesConstants.logger.error("can not find textureSheet $textureSheet you need use ControlableParticleData.registerRenderType() to register mapper") - ParticleRenderType.PARTICLE_SHEET_OPAQUE - } - } - - /** - * 如果你的参数暴露在外面 可能会被服务器调用 - * 则使用这个 - * - * 输入的参数是你的目标的RenderType的toString的内容 - * - * @param value - */ - fun setTextureSheet(value: String) { - this.textureSheet = value - } - - fun setTextureSheet(value: TextureSheetsEnum){ - this.textureSheet = value.name - } - - /** - * 如果你的参数暴露在外面 (可能会被服务器环境调用) - * 那就不要使用这个方法 使用字符串的 - * 因为脑残的设计导致服务器无法访问 ParticleRenderType 类 - * [setTextureSheet(String)] - * - * @param value - */ - fun setTextureSheet(value: ParticleRenderType) { - this.textureSheet = value.toString() - } - - override fun getCodec(): StreamCodec { - return PACKET_CODEC - } - - override fun createControler( - world: ClientLevel, - pos: Vec3, - particleLerpProcess: Float, - posLerpProcess: Float - ): Controlable<*> { - val control = ControlParticleManager.createControl(effect.controlUUID) - val data = this - control.applyInitializedAction { - this.uniformSize = data.uniformSize - this.weightSize = data.weightSize - this.heightSize = data.heightSize - this.depthSize = data.depthSize - this.color = data.color - this.currentAge = data.age - this.lifetime = data.maxAge - this.light = data.light - this.textureSheet = data.getTextureSheet() - this.particleAlpha = data.alpha - this.cameraOption = data.cameraOption - this.axis = data.axis - this.previewAxis = data.axis - this.previewWeightSize = data.weightSize - this.previewHeightSize = data.heightSize - this.previewDepthSize = data.depthSize - this.currentPitch = data.pitch - this.currentYaw = data.yaw - this.currentRoll = data.roll - this.previewPitch = data.pitch - this.previewYaw = data.yaw - this.previewRoll = data.roll - } - return control - } - - override fun getDisplayer(): ParticleDisplayer { - return ParticleDisplayer.withSingle(effect) - } - - override fun clone(): ControlableParticleData { - return ControlableParticleData().also(::copyTo) - } - - /** - * 把可克隆的基础字段复制到另一个 data,并为目标生成新 UUID。 - * - * Example: 扩展类可调用 `super.copyTo(target)` 后补充自己的字段。 - * Forbidden: 网络解码不能调用此方法,因为网络中的 UUID 必须原样保留。 - * - * @param target 接收当前基础字段的实例 - */ - protected fun copyTo(target: ControlableParticleData) { - target.uuid = UUID.randomUUID() - target.velocity = velocity - target.uniformSize = uniformSize - target.weightSize = weightSize - target.heightSize = heightSize - target.depthSize = depthSize - target.color = color - target.alpha = alpha - target.visibleRange = visibleRange - target.age = age - target.maxAge = maxAge - target.effect = effect.clone() - target.textureSheet = textureSheet - target.speed = speed - target.sign = sign - target.speedLimit = speedLimit - target.light = light - target.yaw = yaw - target.pitch = pitch - target.roll = roll - target.cameraOption = cameraOption - target.axis = axis - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt deleted file mode 100644 index 762af1a4..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/DisplayEntityEmittersData.kt +++ /dev/null @@ -1,144 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -import cn.coostack.cooparticlesapi.api.controler.Controlable -import cn.coostack.cooparticlesapi.api.controler.SerializableData -import cn.coostack.cooparticlesapi.CooParticlesAPI -import cn.coostack.cooparticlesapi.display.DisplayEntityManager -import cn.coostack.cooparticlesapi.display.DisplayEntity -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import io.netty.buffer.Unpooled -import net.minecraft.core.RegistryAccess -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * SerializableData wrapper for DisplayEntity emitters entry. - */ -class DisplayEntityEmittersData( - var entityType: String = "", - var entityData: ByteArray = ByteArray(0), - var visibleRange: Float = 256f -) : SerializableData { - constructor(entity: DisplayEntity) : this() { - setEntity(entity) - } - - companion object { - @JvmStatic - val PACKET_CODEC: StreamCodec = - StreamCodec.of( - { buf, data -> - buf.writeUtf(data.entityType) - buf.writeInt(data.entityData.size) - buf.writeBytes(data.entityData) - buf.writeFloat(data.visibleRange) - }, - { buf -> - val type = buf.readUtf() - val size = buf.readInt() - val bytes = ByteArray(size) - buf.readBytes(bytes) - val visibleRange = buf.readFloat() - DisplayEntityEmittersData(type, bytes, visibleRange).also { - it.registryAccess = buf.registryAccess() - } - } - ) - - @JvmStatic - fun fromEntity(entity: DisplayEntity): DisplayEntityEmittersData { - val registryAccess = entity.world?.registryAccess() ?: registryAccessFallback() - val buf = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) - entity.getCodec().encode(buf, entity) - val data = ByteArray(buf.readableBytes()) - buf.readBytes(data) - return DisplayEntityEmittersData(entity::class.java.name, data).also { - it.registryAccess = registryAccess - } - } - - private fun registryAccessFallback(): RegistryAccess { - return CooParticlesAPI.registryAccessOrNull - ?: RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY) - } - } - - private var template: DisplayEntity? = null - private var preparedControler: DisplayEntity? = null - private var registryAccess: RegistryAccess? = null - - private fun decodeEntity(registryAccess: RegistryAccess): DisplayEntity { - val codec = DisplayEntityManager.registeredTypes[entityType] - ?: throw IllegalStateException("DisplayEntity codec not registered for type: $entityType") - return codec.decode( - RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(entityData.copyOf()), registryAccess) - ) - } - - private fun cloneEntity(entity: DisplayEntity): DisplayEntity { - val type = entity::class.java - val ins = runCatching { - type.getDeclaredConstructor(Vec3::class.java, Level::class.java) - .apply { isAccessible = true } - .newInstance(entity.pos, entity.world) - }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() - - return ins.apply { update(entity) } - } - - private fun resolveEntity(registryAccess: RegistryAccess? = null): DisplayEntity { - return template ?: decodeEntity( - registryAccess ?: this.registryAccess ?: registryAccessFallback() - ).also { template = it } - } - - private fun createEntity(registryAccess: RegistryAccess? = null): DisplayEntity { - return cloneEntity(resolveEntity(registryAccess)) - } - - override fun getCodec(): StreamCodec { - return PACKET_CODEC - } - - override fun clone(): SerializableData { - val entity = runCatching { cloneEntity(resolveEntity()) }.getOrNull() - return if (entity != null) { - DisplayEntityEmittersData(entity).also { it.visibleRange = visibleRange } - } else { - DisplayEntityEmittersData(entityType, entityData.copyOf(), visibleRange) - } - } - - override fun createControler( - world: ClientLevel, - pos: Vec3, - particleLerpProcess: Float, - posLerpProcess: Float - ): Controlable<*> { - val entity = createEntity(world.registryAccess()) - entity.world = world - entity.pos = pos - preparedControler = entity - return entity - } - - override fun getDisplayer(): ParticleDisplayer { - val entity = preparedControler ?: createEntity().also { preparedControler = it } - preparedControler = null - return ParticleDisplayer.withDisplayEntity(entity) - } - - fun setEntity(entity: DisplayEntity): DisplayEntityEmittersData { - val clone = cloneEntity(entity) - val data = fromEntity(clone) - entityType = data.entityType - entityData = data.entityData - template = clone - preparedControler = null - return this - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt deleted file mode 100644 index 9f8cf37b..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt +++ /dev/null @@ -1,5 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -fun interface ParticleDataFactory { - fun create(): ControlableParticleData -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt deleted file mode 100644 index 4e28caa7..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmitters.kt +++ /dev/null @@ -1,122 +0,0 @@ -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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.server.level.ServerLevel -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.UUID - - -/** - * 发射单个粒子的 粒子发射器 - * - * 单个粒子通过 ParticleEmitters 修改粒子参数 - * - * -> 不固定数量的ParticleStyleData - * -> 方便输入的 - */ -interface ParticleEmitters : ServerControler, NetworkDirtyMarkable { - var pos: Vec3 - var world: Level? - var tick: Int - - /** - * 当maxTick == -1时 - * 代表此粒子不会由生命周期控制 - */ - var maxTick: Int - var delay: Int - var uuid: UUID - var canceled: Boolean - var playing: Boolean - - /** - * @param innerClass 是否在类内添加 - * 在类内添加的event handler不会参与codec传输 - */ - 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) - - /** - * 注册时传入的CODEC必须和这个完全一致 - * - * 编解码器 - * - * 编码粒子信息, 当前位置 - */ - fun getCodec(): StreamCodec - - override fun getValue(): ParticleEmitters { - return this - } - - /** - * 标记发射器的完整网络状态需要更新。 - * - * Manager 会在当前服务端 tick 结束时合并发送,同一发射器一 tick 内只会更新一次。 - */ - 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt deleted file mode 100644 index e374097e..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleEmittersManager.kt +++ /dev/null @@ -1,398 +0,0 @@ -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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -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 { - // 已经start的 emitters - val emittersCodec = HashMap>() - - /** - * 服务器拥有 - */ - val serverEmitters = HashMap() - internal val visible = ConcurrentHashMap>() - private val dirtyEmitters = ConcurrentHashMap.newKeySet() - - /** - * 客户端可视 - */ - val clientEmitters = ConcurrentHashMap() - - /** 返回客户端当前可见 Emitter 数。 */ - fun clientEmitterCount(): Int = clientEmitters.size - - /** 返回服务端当前 Emitter 数。 */ - fun serverEmitterCount(): Int = serverEmitters.size - - fun getCodecFromID(id: String): StreamCodec? { - return emittersCodec[id] - } - - - /** - * 在客户端执行 - */ - @JvmStatic - fun register( - id: String, - codec: StreamCodec - ): StreamCodec { - 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 - } - - /** - * 玩家离开服务器时清理其 Emitter 可见缓存,使重连后重新发送创建包。 - * - * @param player 已断开连接的服务端玩家 - */ - 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 registryAccess = emitters.world?.registryAccess() ?: CooParticlesAPI.registryAccessOrNull ?: return ByteArray(0) - val buf = RegistryFriendlyByteBuf( - Unpooled.buffer(), - registryAccess - ) - 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 - } - // 获取instance - 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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt deleted file mode 100644 index 7c9e5981..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt +++ /dev/null @@ -1,23 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -object PhysicConstant { - /** - * 时间单位 tick - */ - const val EARTH_GRAVITY = 0.05 - - /** - * 海平面空气密度 - */ - const val SEA_AIR_DENSITY = 1.225 - - /** - * 粒子物理拖动系数 - */ - const val DRAG_COEFFICIENT = 0.01 - - /** - * 粒子横截面积 - */ - const val CROSS_SECTIONAL_AREA = 0.01 -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt deleted file mode 100644 index 8fda5182..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/SimpleRandomParticleData.kt +++ /dev/null @@ -1,270 +0,0 @@ -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.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import org.joml.Vector3f -import kotlin.math.roundToInt -import kotlin.random.Random - -/** - * 防止老是要手写等 一堆莫名其妙的随机数据而设立的 - * @param maxAge 粒子最大生命周期 - * @param minAge 粒子最小生命周期 - * @param maxCount 粒子最大个数 - * @param minCount 粒子最小个数 - * @param maxSize 粒子最大尺寸 - * @param minSize 粒子最小尺寸 - * @param maxSpeed 粒子最大速度 - * @param minSpeed 粒子最小速度 - * - */ -class SimpleRandomParticleData { - - companion object { - val PACKET_CODEC = StreamCodec.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/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt deleted file mode 100644 index 39fb67ac..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/TransformableCParticleEmitter.kt +++ /dev/null @@ -1,493 +0,0 @@ -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.FriendlyByteBuf -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 - -/** - * 只生成 [ControlableCParticleData] 的可变换 GPU 粒子发射器。 - * - * 每个实例拥有独立的 GPU 坐标空间。[space] 为 [CParticleEmitterSpace.LOCAL] 时,已经生成的粒子会随 - * 发射器一起平移、按 [emitterRotation] 旋转并等比缩放;[particleRotation] 只控制新粒子的出生方向。 - * 切换到 [CParticleEmitterSpace.WORLD] 后,新粒子改用世界坐标, - * 行为与 [ClassParticleEmitters] 的 GPU 路径一致。两类粒子使用不同 system,切换模式不会移动旧粒子。 - * - * 本类型直接实现 [ParticleEmitters],不继承 [ClassParticleEmitters]。逐粒子控制器、粒子事件和死亡重生 - * 不属于 GPU system 能力,因此不提供 CPU 回退。 - * - * @param pos 发射器世界坐标 - * @param world 发射器所在世界 - */ -abstract class TransformableCParticleEmitter( - pos: Vec3, - override var world: Level?, -) : ParticleEmitters { - - /** 发射器当前的世界坐标;直接修改后最迟在下一次 tick 同步。 */ - private val posState = dirty(pos) - override var pos by posState - - /** 当前生命周期 tick。 */ - override var tick: Int = 0 - - /** 生命周期上限;`-1` 表示不按 tick 自动结束。 */ - override var maxTick: Int = 120 - - /** 两次发射之间的 tick 间隔;小于 `1` 时按每 tick 发射处理。 */ - override var delay: Int = 0 - - /** 跨服务端和客户端识别同一发射器的 UUID。 */ - override var uuid: UUID = UUID.randomUUID() - - /** 是否已经结束生命周期。 */ - override var canceled: Boolean = false - - /** 是否已经启动。 */ - override var playing: Boolean = false - - /** 新粒子使用的坐标空间。切换不会改变已经生成的粒子。 */ - var space: CParticleEmitterSpace = CParticleEmitterSpace.LOCAL - - /** LOCAL 粒子所在的发射器空间旋转;变化时会带动已经生成的粒子。 */ - var emitterRotation: Quaternionf = Quaternionf() - set(value) { - require(value.isFinite && value.lengthSquared() > 1e-12F) { - "rotation must be finite and non-zero" - } - field = Quaternionf(value).normalize() - } - - /** 新生成粒子的出生位置和初速度旋转;不会改变已经生成的粒子。 */ - var particleRotation: Quaternionf = Quaternionf() - set(value) { - require(value.isFinite && value.lengthSquared() > 1e-12F) { - "particle rotation must be finite and non-zero" - } - field = Quaternionf(value).normalize() - } - - /** 发射器局部空间的等比缩放。必须为正数,保证 GPU 模拟可以换算回局部空间。 */ - var scale: Double = 1.0 - set(value) { - require(value.isFinite() && value > 0.0) { "scale must be finite and positive" } - field = value - } - - /** 环境空气密度,用于生成 GPU 阻力。 */ - var airDensity: Double = 0.0 - - /** 每 tick 向世界 Y 负方向施加的重力加速度。 */ - var gravity: Double = 0.0 - - /** 与 [ClassParticleEmitters] 保持一致的质量数据;当前 GPU 力场不会读取它。 */ - var mass: Double = 1.0 - - /** 风力方向。GPU 路径只支持非 relative 的全局风。 */ - var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { it.loadEmitters(this) } - - /** 是否沿发射器上一 tick 到当前 tick 的路径插值生成。 */ - var enableInterpolator: Boolean = false - - /** 发射路径插值器。 */ - var emittersInterpolator: Interpolator = LineEmitterInterpolator().setRefiner(5.0) - - /** 发射器在当前 tick 中产生的世界坐标位移。 */ - var emitterVelocity: Vec3 = Vec3.ZERO - private set - - private var lastTickPos: Vec3 = pos - private var lastSyncedTransform = TransformSnapshot.capture(this) - - /** - * 旧版 GPU 力场列表接口。 - * - * 该类不继承 [ClassParticleEmitters],因此单独保留旧签名。 - * 默认实现由流式接口转发,旧 emitter 覆写此方法仍能参与 GPU 模拟。 - * - * @return 按执行顺序排列的力场列表 - */ - open fun cparticleForces(): List = emptyList() - - /** - * 返回当前 GPU system 使用的附加力场。 - * - * 位置和方向参数都沿用 [ClassParticleEmitters] 的世界坐标语义。 - * 默认实现兼容旧版 [cparticleForces];新 emitter 可直接提交到 sink。 - * - * @param sink 当前 emitter 的力场快照 - */ - open fun submitCParticleForces(sink: CParticleForceSink) { - sink.submitAll(cparticleForces()) - } - - /** 返回方块碰撞网格相对当前发射器中心的保证范围。 */ - open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE - - /** - * 拒绝注册逐粒子事件。 - * - * @param handler 待注册事件处理器 - * @param innerClass 是否为实现类内部处理器 - * @throws UnsupportedOperationException 本发射器没有逐粒子 CPU 控制器 - */ - final override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { - throw UnsupportedOperationException("TransformableCParticleEmitter does not support per-particle events") - } - - override fun start() { - if (playing) return - playing = true - lastTickPos = pos - emitterVelocity = Vec3.ZERO - lastSyncedTransform = TransformSnapshot.capture(this) - if (enableInterpolator) { - emittersInterpolator.insertPoint(pos) - } - } - - override fun stop() { - canceled = true - } - - override fun tick() { - if (canceled || !playing) return - val level = world ?: return - doTick() - emitterVelocity = pos - lastTickPos - lastTickPos = pos - - if (!level.isClientSide) { - syncServerTransformIfChanged() - increaseTick() - return - } - - TransformableCParticleEmitterBridge.syncSystems(this) - if (enableInterpolator) { - emittersInterpolator.insertPoint(pos) - } - if (tick % max(1, delay) == 0) { - if (enableInterpolator) { - val refined = emittersInterpolator.getRefinedResult() - val denominator = (refined.size - 1).coerceAtLeast(1).toFloat() - refined.forEachIndexed { index, location -> - val spawnPos = location.toVector() - val lerpProgress = if (refined.size == 1) 1F else index / denominator - doSubtick(spawnPos, lerpProgress) - spawnParticle(spawnPos, lerpProgress) - } - } else { - spawnParticle(pos, 1F) - } - } - increaseTick() - } - - override fun spawnParticle(pos: Vec3, lerpProgress: Float) { - val level = world as? ClientLevel ?: return - val player = Minecraft.getInstance().player ?: return - val particles = genParticles(lerpProgress) - val batchSize = particles.size - particles.forEach { (data, relative) -> - val worldPosition = TransformableCParticleEmitterBridge.resolveWorldPosition( - this, - pos, - relative, - ) ?: return@forEach - if (player.position().distanceTo(worldPosition) > data.visibleRange) return@forEach - TransformableCParticleEmitterBridge.trySpawn( - this, - level, - pos, - relative, - data, - batchSize, - ) - } - } - - /** - * 服务器和客户端每 tick 都会调用,用于更新发射器自身状态。 - * - * 实现必须保证两端执行结果可同步,不能在这里直接访问仅客户端存在的渲染对象。 - */ - abstract fun doTick() - - /** - * 生成本次发射的 GPU 粒子及其相对位置。 - * - * LOCAL 模式下相对位置属于发射器坐标空间;WORLD 模式下会直接加到生成世界坐标。 - * - * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` - * @return 本次要生成的 GPU data 和位置 - */ - abstract fun genParticles(lerpProgress: Float): List> - - /** - * 在路径插值中的一次发射前调用。 - * - * @param current 本次插值后的发射世界坐标 - * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` - */ - protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - - /** - * 追加发射器空间旋转。 - * - * LOCAL 模式下,已经生成的粒子位置和速度都会跟随整个空间旋转。 - * - * @param rotation 发射器空间的增量四元数 - */ - fun rotateEmitter(rotation: Quaternionfc) { - emitterRotation = Quaternionf(emitterRotation).mul(rotation) - syncClientTransform() - } - - /** 绕发射器局部 Z 轴追加旋转。 */ - fun rotateEmitter(radian: Double) { - rotateEmitter(Quaternionf().rotateLocalZ(radian.toFloat())) - } - - /** - * 追加新粒子的出生旋转。 - * - * 只改变后续新粒子的出生位置和初速度;已经生成的粒子保持生成时的运动方向。 - * 粒子四边形朝向仍只由 `cameraOption` 和粒子自身旋转字段决定。 - * - * @param rotation 新粒子出生坐标系的增量四元数 - */ - fun rotateParticle(rotation: Quaternionfc) { - particleRotation = Quaternionf(particleRotation).mul(rotation) - syncClientTransform() - } - - /** 绕新粒子出生坐标系的局部 Z 轴追加旋转。 */ - fun rotateParticle(radian: Double) { - rotateParticle(Quaternionf().rotateLocalZ(radian.toFloat())) - } - - /** - * 按世界坐标增量移动发射器。 - * - * @param offset 本次增加的世界坐标位移 - */ - fun translate(offset: Vec3) { - teleportTo(pos + offset) - } - - /** - * 按世界坐标分量增量移动发射器。 - * - * @param x X 轴增量 - * @param y Y 轴增量 - * @param z Z 轴增量 - */ - fun translate(x: Double, y: Double, z: Double) { - translate(Vec3(x, y, z)) - } - - /** - * 设置绝对等比缩放。 - * - * @param new 大于零的缩放值 - */ - fun scale(new: Double) { - scale = new - syncClientTransform() - } - - /** - * 按倍率增量缩放。 - * - * @param factor 大于零的缩放倍率 - */ - fun scaleBy(factor: Double) { - require(factor.isFinite() && factor > 0.0) { "scale factor must be finite and positive" } - scale(scale * factor) - } - - override fun teleportTo(to: Vec3) { - pos = to - syncClientTransform() - } - - override fun rotateToPoint(to: RelativeLocation) { - val next = Quaternionf() - Math3DUtil.rotateQuatToPoint(next, to) - emitterRotation = next - syncClientTransform() - } - - override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { - val next = Quaternionf() - Math3DUtil.rotateQuatToPoint(next, to) - next.rotateLocalZ(radian.toFloat()) - emitterRotation = next - syncClientTransform() - } - - override fun rotateAsAxis(radian: Double) { - rotateEmitter(radian) - } - - override fun update(emitters: ParticleEmitters) { - if (emitters !is TransformableCParticleEmitter || getEmittersID() != emitters.getEmittersID()) return - posState.setCodecValue(emitters.pos) - world = world ?: emitters.world - tick = emitters.tick - maxTick = emitters.maxTick - delay = emitters.delay - uuid = emitters.uuid - canceled = emitters.canceled - playing = emitters.playing - space = emitters.space - emitterRotation = Quaternionf(emitters.emitterRotation) - particleRotation = Quaternionf(emitters.particleRotation) - scale = emitters.scale - gravity = emitters.gravity - airDensity = emitters.airDensity - mass = emitters.mass - wind = emitters.wind.also { it.loadEmitters(this) } - enableInterpolator = emitters.enableInterpolator - emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) - ParticleEmittersRegistryHelper.updateEmitter(this, emitters) - resetRuntimeState(addInterpolatorPoint = false) - syncClientTransform() - } - - /** 结束后停止接收新粒子,并在已有粒子死亡后释放专属 GPU systems。 */ - internal fun finishClientSystems() { - TransformableCParticleEmitterBridge.finishEmitter(uuid) - } - - private fun syncClientTransform() { - if (world?.isClientSide == true) { - TransformableCParticleEmitterBridge.syncSystems(this) - } - } - - private fun syncServerTransformIfChanged() { - val current = TransformSnapshot.capture(this) - if (current == lastSyncedTransform) return - markDirty() - lastSyncedTransform = current - } - - private fun increaseTick() { - if (++tick >= maxTick && maxTick != -1) { - stop() - } - } - - private fun resetRuntimeState(addInterpolatorPoint: Boolean) { - lastTickPos = pos - emitterVelocity = Vec3.ZERO - lastSyncedTransform = TransformSnapshot.capture(this) - if (addInterpolatorPoint && enableInterpolator) { - emittersInterpolator.insertPoint(pos) - } - } - - private data class TransformSnapshot( - val pos: Vec3, - val rotationX: Float, - val rotationY: Float, - val rotationZ: Float, - val rotationW: Float, - val particleRotationX: Float, - val particleRotationY: Float, - val particleRotationZ: Float, - val particleRotationW: Float, - val scale: Double, - val space: CParticleEmitterSpace, - ) { - companion object { - fun capture(emitter: TransformableCParticleEmitter): TransformSnapshot { - val rotation = emitter.emitterRotation - return TransformSnapshot( - emitter.pos, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - emitter.particleRotation.x, - emitter.particleRotation.y, - emitter.particleRotation.z, - emitter.particleRotation.w, - emitter.scale, - emitter.space, - ) - } - } - } - - companion object { - /** 编码所有可变换 GPU emitter 共用的网络字段。 */ - fun encodeBase(data: TransformableCParticleEmitter, buf: FriendlyByteBuf) { - 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.writeByte(data.space.networkId) - buf.writeQuaternion(data.emitterRotation) - buf.writeQuaternion(data.particleRotation) - buf.writeDouble(data.scale) - 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) - } - - /** 解码所有可变换 GPU emitter 共用的网络字段。 */ - fun decodeBase(container: TransformableCParticleEmitter, buf: FriendlyByteBuf) { - container.posState.setCodecValue(buf.readVec3()) - container.tick = buf.readInt() - container.maxTick = buf.readInt() - container.delay = buf.readInt() - container.uuid = buf.readUUID() - container.canceled = buf.readBoolean() - container.playing = buf.readBoolean() - container.space = CParticleEmitterSpace.fromNetworkId(buf.readUnsignedByte().toInt()) - container.emitterRotation = buf.readQuaternion() - container.particleRotation = buf.readQuaternion() - container.scale = buf.readDouble() - container.gravity = buf.readDouble() - container.airDensity = buf.readDouble() - container.mass = buf.readDouble() - container.enableInterpolator = buf.readBoolean() - container.emittersInterpolator.setRefiner(buf.readDouble()) - val windId = buf.readUtf() - container.wind = WindDirections.getCodecFromID(windId).decode(buf).also { - it.loadEmitters(container) - } - container.resetRuntimeState(addInterpolatorPoint = true) - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt deleted file mode 100644 index 91991286..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt +++ /dev/null @@ -1,30 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class OrbitMode { - /** - * 物理轨道(默认) - * - * - 切向 + 径向纠正 - * - 手感最真实 - * - 需要限幅,否则启动阶段可能不稳定 - */ - PHYSICAL, - - /** - * 弹簧轨道 - * - * - 半径误差像弹簧一样回弹 - * - 非常稳定,不容易飞走 - * - 手感偏“设计感” - */ - SPRING, - - /** - * 吸附轨道 - * - * - 每 tick 直接把粒子拉到目标半径附近 - * - 最稳定,最不物理 - * - 适合魔法阵 / UI / 装饰粒子 - */ - SNAP -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt deleted file mode 100644 index eeee7078..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt +++ /dev/null @@ -1,128 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.max - -/** - * 吸引/排斥力(指向目标点的加速度) - * - * - strength > 0:吸引(往目标点拉) - * - strength < 0:排斥(从目标点推开) - * - falloff 决定“近强远弱”的衰减方式 - */ -class ParticleAttractionCommand() : ParticleCommand { - - /** 目标点(世界坐标)。 */ - var target: Supplier = Supplier { Vec3.ZERO } - - /** - * 强度(每 tick 的速度增量基准)。 - * - * - 0.1 ~ 0.5:轻微牵引(像磁力) - * - 0.5 ~ 3.0:明显吸附(像黑洞) - * - 3.0+:非常暴力(需要配合 speedLimit / damping) - * - * 负数则变成排斥力。 - */ - var strength: Double = 0.8 - - /** - * 有效范围尺度(>0)。 - * - * - 越大:衰减越慢,远处也能感受到吸力 - * - 越小:只有靠近目标才明显 - */ - var range: Double = 8.0 - - /** - * 衰减幂次(>=1)。 - * - * - 1:衰减较缓 - * - 2:更像“平方反比”趋势 - * - 3:近距离极强,远处几乎无 - */ - var falloffPower: Double = 2.0 - - /** - * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 - * - * - 建议 0.1 ~ 1.0 - */ - var minDistance: Double = 0.25 - - /** - * @param target 目标点(世界坐标) - * @param strength 强度(>0吸引 <0排斥) - * @param range 范围尺度,越大影响越远 - * @param falloffPower 衰减幂次(>=1) - * @param minDistance 最小距离钳制,防止爆炸/NaN - */ - constructor( - target: Supplier = Supplier { Vec3.ZERO }, - strength: Double = 0.8, - range: Double = 8.0, - falloffPower: Double = 2.0, - minDistance: Double = 0.25, - ) : this() { - this.target = target - this.strength = strength - this.range = range - this.falloffPower = falloffPower - this.minDistance = minDistance - } - - /** 目标点(世界坐标)。 */ - fun target(v: Supplier) = apply { target = v } - - /** - * 强度(每 tick 的速度增量基准)。 - * - * - 0.1 ~ 0.5:轻微牵引(像磁力) - * - 0.5 ~ 3.0:明显吸附(像黑洞) - * - 3.0+:非常暴力(需要配合 speedLimit / damping) - * - * 负数则变成排斥力。 - */ - fun strength(v: Double) = apply { strength = v } - - /** - * 有效范围尺度(>0)。 - * - * - 越大:衰减越慢,远处也能感受到吸力 - * - 越小:只有靠近目标才明显 - */ - fun range(v: Double) = apply { range = v } - - /** - * 衰减幂次。 - * - * - 1:衰减较缓 - * - 2:更像“平方反比”趋势 - * - 3:近距离极强,远处几乎无 - */ - fun falloffPower(v: Double) = apply { falloffPower = v } - - /** - * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 - * - * - 建议 0.1 ~ 1.0 - */ - fun minDistance(v: Double) = apply { minDistance = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val dir = target.get().subtract(pos) - var d = dir.length() - if (d < 1e-9) return - d = max(d, minDistance) - - val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) - val dv = dir.normalize().scale(strength * falloff) - - data.velocity = data.velocity.add(dv) - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt deleted file mode 100644 index 74acb430..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle - - -fun interface ParticleCommand { - /** - * 粒子“命令/模块”接口:用于在粒子每个 tick 更新前,对粒子数据进行一次可组合的逻辑处理。 - * - * ## 设计目的 - * 你可以把它理解成 **Unity 粒子系统的一个 Module**(例如:旋转力、阻尼、吸引力、漩涡、流场等): - * - 每个 module 都实现一个 `execute(...)` - * - 在粒子 tick 之前按顺序调用这些 module - * - module 之间通过“修改同一个 data”来叠加效果(通常是 `data.velocity`、`data.yaw/pitch/roll` 等) - * - * ## 调用时机(强烈建议) - * 推荐在 `Emitter.singleParticleAction(...)` 中,将命令挂载到 `ParticleControler.addPreTickAction { ... }`: - * - 这样命令会在 **每个 tick** 都执行一次(而不是只在生成时执行一次) - * - 且执行时你能拿到 “实时的粒子实例状态”(`particle.loc`, `particle.onTheGround`, `particle.currentAge` 等) - * - * ## 参数说明 - * @param data - * 可变的粒子控制数据(你自己的控制层数据)。 - * - **常见修改点**:`data.velocity`(加速度/阻尼/流场)、`data.faceToCamera`、`data.yaw/pitch/roll` 等 - * - 注意:`ControlableParticleData` 中有一部分字段在粒子生成后只生效一次(例如颜色/纹理等), - * 因此命令更适合修改“每 tick 生效”的字段(通常是 velocity 与旋转等)。 - * - * @param particle - * 当前 tick 正在更新的粒子实例(客户端真实粒子对象)。 - * - **常用读取**:`particle.loc`(位置)、`particle.onTheGround`(是否落地)、`particle.currentAge`(年龄)等 - * - **注意**:一般不建议在命令里直接 teleport 粒子位置;更推荐通过 `data.velocity` 影响运动, - * 由发射器的 physics/move 逻辑统一处理。 - * - * ## 组合与顺序 - * 多个命令按添加顺序执行: - * - 前面的命令可能会影响后面的命令输入(例如先吸引再阻尼 vs 先阻尼再吸引) - * - 如果你在命令里做了 clamp/limit(例如限速),通常建议放到最后一个命令执行 - * - * ## 编写建议(实践约定) - * - **保持轻量**:命令每 tick 对每个粒子都会执行,避免昂贵计算/分配对象(尤其是大量粒子场景) - * - **不要产生 NaN**:对距离接近 0 的情况要做钳制(例如 minDistance) - * - **可复用**:命令实例最好可以被多个粒子共享(内部不要缓存“单粒子状态”) - */ - fun execute(data: ControlableParticleData, particle: ControlableParticle) -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt deleted file mode 100644 index 9ad00fcb..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt +++ /dev/null @@ -1,120 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle - -/** - * `ParticleCommand` 的顺序队列:用于把多个“粒子模块/预设”组合成一个可复用的执行链。 - * - * ## 设计目的 - * 让你用非常接近 Unity 粒子系统 “模块叠加”的方式组织效果: - * - 每个 module 都是一个 `ParticleCommand` - * - 通过队列把多个 module 组装起来 - * - 每 tick 调一次 `applyVelocity(...)`,队列会按顺序把所有 module 应用到同一个粒子上 - * - * ## 典型使用方式 - * 在 `Emitter.singleParticleAction(...)` 里把队列挂到粒子 controller 的 preTick: - * ```kotlin - * val queue = ParticleCommandQueue() - * .add(AirDragCommand(0.15)) - * .add(AttractionCommand(target = center, strength = 0.8)) - * - * override fun singleParticleAction( - * controler: ParticleControler, - * data: ControlableParticleData, - * spawnPos: RelativeLocation, - * spawnWorld: Level, - * particleLerpProgress: Float, - * posLerpProgress: Float - * ) { - * controler.addPreTickAction { - * queue.applyVelocity(data, this) - * } - * } - * ``` - */ -class ParticleCommandQueue { - val commands = - ArrayDeque Boolean>>() - - @Suppress("UNCHECKED_CAST") - private val alwaysExecutePredicate = - AlwaysExecutePredicate as ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean - - /** - * 对当前粒子执行队列中的所有命令。 - * - * @param data 当前粒子的可变控制数据(被命令叠加修改) - * @param particle 当前 tick 正在更新的粒子实例(用于读取实时状态) - */ - fun applyVelocity(data: ControlableParticleData, particle: ControlableParticle) { - commands.forEach { - if (it.second(it.first, data, particle)) { - it.first.execute(data, particle) - } - } - } - - /** - * 更新队列中指定索引位置的 Command 数据。 - * - * @param T Command 的具体类型 - * @param applier 对该 Command 执行的更新逻辑(直接修改其内部参数) - * @param index Command 在队列中的索引位置 - */ - fun updateWith(index: Int, applier: T.() -> Unit): ParticleCommandQueue = - apply { - if (index !in commands.indices) { - return@apply - } - val command = commands[index].first - runCatching { applier(command as T) } - } - - /** - * 更新队列中所有符合指定类型的 Command。 - * - * @param T 需要被更新的 Command 类型 - * @param applier 对所有匹配 Command 执行的更新逻辑 - */ - inline fun updateWithTypes(applier: T.() -> Unit): ParticleCommandQueue = apply { - commands.map { it.first }.filterIsInstance() - .forEach { - applier(it) - } - } - - /** - * 向队列末尾追加一个命令,并返回自身以支持链式调用。 - * - * @param command 要追加的粒子命令 - * @return this(便于 `.add(...).add(...)` 链式写法) - */ - fun add(command: ParticleCommand): ParticleCommandQueue { - commands.add(command to alwaysExecutePredicate) - return this - } - - /** - * 向队列末尾追加一个命令,并返回自身以支持链式调用。 - * - * @param command 要追加的粒子命令 - * @param predicate 执行这个命令的条件 - * @return this(便于 `.add(...).add(...)` 链式写法) - */ - fun add( - command: ParticleCommand, - predicate: ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean - ): ParticleCommandQueue { - commands.add(command to predicate) - return this - } - - private object AlwaysExecutePredicate : Function3 { - override fun invoke( - command: ParticleCommand, - data: ControlableParticleData, - particle: Any? - ): Boolean = true - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt deleted file mode 100644 index e7190ae1..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt +++ /dev/null @@ -1,334 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.abs -import kotlin.math.floor - -class ParticleDistortionCommand() : ParticleCommand { - var center: Supplier = Supplier { Vec3.ZERO } - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - var radius: Double = 3.0 - var radialStrength: Double = 0.35 - var axialStrength: Double = 0.25 - var tangentialStrength: Double = 0.0 - var frequency: Double = 0.25 - var timeScale: Double = 0.1 - var phaseOffset: Double = 0.0 - var followStrength: Double = 0.35 - var maxStep: Double = 0.6 - var baseAxial: Double = 0.0 - var seedOffset: Int = 0 - var useLifeCurve: Boolean = false - - /** - * @param center 扰动中心点(轴线穿过的点,世界坐标)。 - * - 建议与发射器中心一致,这样“环”会被稳定地扭曲而不是整体漂移。 - * - 使用 Supplier 便于跟随移动的实体/点位(每 tick 取一次)。 - * - * @param axis 扰动轴(环所在平面的法向/法线方向)。 - * - 会自动 normalize,零向量会回退到 (0,1,0)。 - * - (0,1,0) 表示环在 XZ 平面;(1,0,0) 表示环在 YZ 平面。 - * - * @param radius 基础半径(未扰动前的目标环半径)。 - * - 设为发射器环半径时,扰动主要表现为“扭曲”而不是整体收缩/扩张。 - * - 过小会让环过紧,过大则偏离发射器形状。 - * - * @param radialStrength 径向扰动强度(沿“半径方向”的噪声幅度)。 - * - 控制“鼓包/凹陷”的程度,值越大环越不规则。 - * - 设为 0 表示不做径向扭曲,只保留其它方向扰动。 - * - * @param axialStrength 轴向扰动强度(沿“轴线方向”的噪声幅度)。 - * - 控制环在法向上的起伏(像波浪上下抖动)。 - * - 设为 0 则保持在同一平面内。 - * - * @param tangentialStrength 切向扰动强度(沿“绕环方向”的噪声幅度)。 - * - 会让粒子沿环方向滑移,产生“扭拧/拉扯”的感觉。 - * - 设为 0 可避免环周向被拉散。 - * - * @param frequency 空间频率(噪声随位置变化的密度)。 - * - 越大越“细碎/抖动”,越小越“平滑/大块”。 - * - 影响扭曲的纹理尺度。 - * - * @param timeScale 时间变化速度(噪声随时间滚动的快慢)。 - * - 0 表示静态扭曲;越大越“流动/抖动”。 - * - 过大可能出现闪烁感。 - * - * @param phaseOffset 相位偏移(用来错开不同发射器/批次的噪声)。 - * - 不改变强度,只改变噪声的“起点”。 - * - * @param followStrength 追随强度(粒子被拉回扭曲环的力度)。 - * - 越大环越“紧”,不易发散;越小越“松”,容易漂离形状。 - * - * @param maxStep 单 tick 最大修正步长(对速度增量做上限)。 - * - 防止拉回过猛导致抖动或穿透。 - * - <=0 表示不限制。 - * - * @param baseAxial 基础轴向偏移(整体沿轴线的固定偏移量)。 - * - 用于把整个环抬高/降低或制作“偏心”的扭曲面。 - * - * @param seedOffset 噪声种子偏移(同一效果的“变体”)。 - * - 相同 seedOffset 会得到相同扭曲纹理;不同值能做多种随机样式。 - * - * @param useLifeCurve 是否按生命周期衰减扰动强度。 - * - true:出生强、死亡弱,更自然。 - * - false:全生命周期强度恒定,更稳定。 - */ - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - radius: Double = 3.0, - radialStrength: Double = 0.35, - axialStrength: Double = 0.25, - tangentialStrength: Double = 0.0, - frequency: Double = 0.25, - timeScale: Double = 0.1, - phaseOffset: Double = 0.0, - followStrength: Double = 0.35, - maxStep: Double = 0.6, - baseAxial: Double = 0.0, - seedOffset: Int = 0, - useLifeCurve: Boolean = false, - ) : this() { - this.center = center - this.axis = axis - this.radius = radius - this.radialStrength = radialStrength - this.axialStrength = axialStrength - this.tangentialStrength = tangentialStrength - this.frequency = frequency - this.timeScale = timeScale - this.phaseOffset = phaseOffset - this.followStrength = followStrength - this.maxStep = maxStep - this.baseAxial = baseAxial - this.seedOffset = seedOffset - this.useLifeCurve = useLifeCurve - } - - /** - * 设置扰动中心点(轴线穿过的点,世界坐标)。 - * - * - 建议与发射器中心一致,保证环形整体不漂移。 - * - Supplier 可用于跟随移动对象,每 tick 自动读取最新位置。 - */ - fun center(v: Supplier) = apply { center = v } - - /** - * 设置扰动轴(环所在平面的法线方向)。 - * - * - 会在计算时 normalize。 - * - (0,1,0) 为 XZ 平面,(1,0,0) 为 YZ 平面。 - */ - fun axis(v: Vec3) = apply { axis = v } - - /** - * 设置基础半径(未扰动前的目标环半径)。 - * - * - 与发射器环半径一致时最稳定。 - * - 过小会让环“收紧”,过大可能偏离发射器形状。 - */ - fun radius(v: Double) = apply { radius = v } - - /** - * 设置径向扰动强度(沿半径方向的扭曲幅度)。 - * - * - 越大越“鼓包/凹陷”,环越不规则。 - * - 设为 0 可保持圆环半径稳定。 - */ - fun radialStrength(v: Double) = apply { radialStrength = v } - - /** - * 设置轴向扰动强度(沿轴线方向的起伏幅度)。 - * - * - 控制环在法向上的上下波动。 - * - 设为 0 则保持在同一平面内。 - */ - fun axialStrength(v: Double) = apply { axialStrength = v } - - /** - * 设置切向扰动强度(沿环方向的滑移幅度)。 - * - * - 让粒子沿环周向“拉扯/扭拧”。 - * - 过大可能导致环被拉散。 - */ - fun tangentialStrength(v: Double) = apply { tangentialStrength = v } - - /** - * 设置空间频率(噪声随位置变化的密度)。 - * - * - 越大越细碎、越抖;越小越平滑、越大块。 - * - 决定扭曲纹理的尺度。 - */ - fun frequency(v: Double) = apply { frequency = v } - - /** - * 设置时间变化速度(噪声滚动速度)。 - * - * - 0 表示静态扭曲。 - * - 越大越活跃,但过大容易“闪烁抖动”。 - */ - fun timeScale(v: Double) = apply { timeScale = v } - - /** - * 设置相位偏移(错开不同发射器/批次的噪声)。 - * - * - 不影响强度,只改变噪声起点。 - * - 用于避免多个环“同步抖动”。 - */ - fun phaseOffset(v: Double) = apply { phaseOffset = v } - - /** - * 设置追随强度(粒子被拉回扭曲环的力度)。 - * - * - 越大越“紧”,不易发散。 - * - 越小越“松”,允许更自由的漂移。 - */ - fun followStrength(v: Double) = apply { followStrength = v } - - /** - * 设置单 tick 最大修正步长(速度增量上限)。 - * - * - 防止拉回过猛导致抖动或穿透。 - * - <=0 表示不限制。 - */ - fun maxStep(v: Double) = apply { maxStep = v } - - /** - * 设置基础轴向偏移(整体沿轴线的固定偏移量)。 - * - * - 可把环整体抬高/降低。 - * - 配合轴向扰动可做“偏心/倾移”的扭曲环。 - */ - fun baseAxial(v: Double) = apply { baseAxial = v } - - /** - * 设置噪声种子偏移(同效果的随机变体)。 - * - * - 相同 seedOffset -> 相同扭曲纹理。 - * - 不同 seedOffset -> 不同随机外观。 - */ - fun seedOffset(v: Int) = apply { seedOffset = v } - - /** - * 设置是否按生命周期衰减扰动强度。 - * - * - true:出生强、死亡弱,更自然。 - * - false:全生命周期恒定,更稳定。 - */ - fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val centerPos = center.get() - val ax = safeNormalize(axis) - val pos = particle.loc - val r = pos.subtract(centerPos) - val axialComp = ax.scale(r.dot(ax)) - var radial = r.subtract(axialComp) - var radialLen = radial.length() - if (radialLen < 1e-9) { - radial = anyPerp(ax) - radialLen = 1.0 - } else { - radial = radial.scale(1.0 / radialLen) - } - - var tangent = ax.cross(radial) - val tLen = tangent.length() - tangent = if (tLen < 1e-9) anyPerp(ax) else tangent.scale(1.0 / tLen) - - val lifeT = if (particle.lifetime > 0) { - particle.currentAge.toDouble() / particle.lifetime.toDouble() - } else { - 0.0 - } - val lifeMul = if (useLifeCurve) (1.0 - lifeT).coerceIn(0.0, 1.0) else 1.0 - - val time = particle.currentAge.toDouble() * timeScale + phaseOffset - val local = pos.subtract(centerPos) - val p = local.scale(frequency).add(time, time * 0.7, time * 1.3) - val seed = particle.controlUUID.hashCode() + seedOffset - - val nr = noise(p, seed + 11) - val na = noise(p, seed + 23) - val nt = noise(p, seed + 37) - - val targetRadius = (radius + nr * radialStrength * lifeMul).coerceAtLeast(0.0) - val targetAxial = baseAxial + na * axialStrength * lifeMul - val targetTangential = nt * tangentialStrength * lifeMul - - val targetPos = centerPos - .add(ax.scale(targetAxial)) - .add(radial.scale(targetRadius)) - .add(tangent.scale(targetTangential)) - - var dv = targetPos.subtract(pos).scale(followStrength) - val dvLen = dv.length() - if (maxStep > 0.0 && dvLen > maxStep) { - dv = dv.scale(maxStep / dvLen) - } - data.velocity = data.velocity.add(dv) - } - - private fun safeNormalize(v: Vec3): Vec3 { - val len = v.length() - return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) - } - - private fun anyPerp(axis: Vec3): Vec3 { - val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) - val perp = axis.cross(ref) - val len = perp.length() - return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) - } - - private fun noise(p: Vec3, seed: Int): Double { - return valueNoise3(p, seed) * 2.0 - 1.0 - } - - private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) - private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t - - private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { - var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 - n = (n xor (n ushr 13)) * 1274126177 - n = n xor (n ushr 16) - return (n and 0x7fffffff).toDouble() / 2147483647.0 - } - - private fun valueNoise3(p: Vec3, seed: Int): Double { - val x0 = floor(p.x).toInt() - val y0 = floor(p.y).toInt() - val z0 = floor(p.z).toInt() - val fx = p.x - x0 - val fy = p.y - y0 - val fz = p.z - z0 - - val u = fade(fx) - val v = fade(fy) - val w = fade(fz) - - fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) - - val n000 = h(0, 0, 0) - val n100 = h(1, 0, 0) - val n010 = h(0, 1, 0) - val n110 = h(1, 1, 0) - val n001 = h(0, 0, 1) - val n101 = h(1, 0, 1) - val n011 = h(0, 1, 1) - val n111 = h(1, 1, 1) - - val nx00 = lerp(n000, n100, u) - val nx10 = lerp(n010, n110, u) - val nx01 = lerp(n001, n101, u) - val nx11 = lerp(n011, n111, u) - - val nxy0 = lerp(nx00, nx10, v) - val nxy1 = lerp(nx01, nx11, v) - - return lerp(nxy0, nxy1, w) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt deleted file mode 100644 index e908304b..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt +++ /dev/null @@ -1,99 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 - -/** - * 空气阻尼(Drag) - * - * 每 tick 把速度乘上 (1 - drag),让粒子慢下来、更“黏”。 - */ -class ParticleDragCommand() : ParticleCommand { - /** - * 阻尼强度(>=0)。 - * - * - 0.0:完全不阻尼 - * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) - * - 0.2 ~ 1.0:明显刹车(碎屑、火花) - * - >1.0:非常快停(像在胶水里) - */ - var damping: Double = 0.15 - - /** - * 最小速度阈值(<=0 表示不使用)。 - * - * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 - */ - var minSpeed: Double = 0.0 - - /** - * 额外的线性阻力(每 tick 额外减去 speed * linear) - * - * - 推荐保持 0,除非你想要“低速更粘”的感觉 - */ - var linear: Double = 0.0 - - /** - * @param damping 阻尼强度(>=0),越大速度衰减越快 - * @param minSpeed 低于该速度则直接归零(<=0 不启用) - * @param linear 线性阻力项(建议 0) - */ - constructor( - damping: Double = 0.15, - minSpeed: Double = 0.0, - linear: Double = 0.0, - ) : this() { - this.damping = damping - this.minSpeed = minSpeed - this.linear = linear - } - - /** - * 阻尼强度(>=0)。 - * - * - 0.0:完全不阻尼 - * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) - * - 0.2 ~ 1.0:明显刹车(碎屑、火花) - * - >1.0:非常快停(像在胶水里) - */ - fun damping(v: Double) = apply { damping = v } - - /** - * 最小速度阈值(<=0 表示不使用)。 - * - * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 - */ - fun minSpeed(v: Double) = apply { minSpeed = v } - - /** - * 额外的线性阻力(每 tick 额外减去 speed * linear) - * - * - 推荐保持 0,除非你想要“低速更粘”的感觉 - */ - fun linear(v: Double) = apply { linear = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - var v = data.velocity - val speed = v.length() - if (minSpeed > 0 && speed <= minSpeed) { - data.velocity = Vec3.ZERO - return - } - - // exp damping - val factor = GraphMathHelper.expDampFactor(damping, 1.0) - v = v.scale(factor) - - // optional linear drag - if (linear > 0.0) { - val s2 = v.length() - if (s2 > 1e-9) { - val shrink = (1.0 - linear).coerceIn(0.0, 1.0) - v = v.scale(shrink) - } - } - data.velocity = v - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt deleted file mode 100644 index b50e6fb9..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt +++ /dev/null @@ -1,119 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import kotlin.math.cos -import kotlin.math.sin - -/** - * 流场:根据粒子位置/时间生成一个“风向量”,并叠加到速度里。 - * - * 这个实现是解析型流场(sin/cos),特点: - * - 计算非常快 - * - 通过 frequency / amplitude / timeScale 能做出很多“丝带/涡流”效果 - */ -class ParticleFlowFieldCommand() : ParticleCommand { - - /** - * 振幅(每 tick 的速度增量幅度) - * - * - 0.01 ~ 0.1:微风(烟雾漂移) - * - 0.1 ~ 0.8:明显流动(能看出纹理) - * - >0.8:很强(容易乱飞,需要阻尼/限速) - */ - var amplitude: Double = 0.15 - - /** - * 空间频率(越大纹理越密,越小越平缓) - * - * - 0.05 ~ 0.2:大尺度流场 - * - 0.2 ~ 1.0:中小尺度卷曲 - */ - var frequency: Double = 0.25 - - /** - * 时间缩放(控制流场随时间变化速度) - * - * - 0:静态流场(像固定风) - * - 0.02 ~ 0.2:缓慢变动 - * - >0.3:快速抖动/波动 - */ - var timeScale: Double = 0.06 - - /** - * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) - */ - var phaseOffset: Double = 0.0 - - /** - * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) - */ - var worldOffset: Vec3 = Vec3.ZERO - - constructor( - amplitude: Double = 0.15, - frequency: Double = 0.25, - timeScale: Double = 0.06, - phaseOffset: Double = 0.0, - worldOffset: Vec3 = Vec3.ZERO, - ) : this() { - this.amplitude = amplitude - this.frequency = frequency - this.timeScale = timeScale - this.phaseOffset = phaseOffset - this.worldOffset = worldOffset - } - - /** - * 振幅(每 tick 的速度增量幅度) - * - * - 0.01 ~ 0.1:微风(烟雾漂移) - * - 0.1 ~ 0.8:明显流动(能看出纹理) - * - >0.8:很强(容易乱飞,需要阻尼/限速) - */ - fun amplitude(v: Double) = apply { amplitude = v } - - /** - * 空间频率(越大纹理越密,越小越平缓) - * - * - 0.05 ~ 0.2:大尺度流场 - * - 0.2 ~ 1.0:中小尺度卷曲 - */ - fun frequency(v: Double) = apply { frequency = v } - - /** - * 时间缩放(控制流场随时间变化速度) - * - * - 0:静态流场(像固定风) - * - 0.02 ~ 0.2:缓慢变动 - * - >0.3:快速抖动/波动 - */ - fun timeScale(v: Double) = apply { timeScale = v } - - /** - * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) - */ - fun phaseOffset(v: Double) = apply { phaseOffset = v } - - /** - * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) - */ - fun worldOffset(v: Vec3) = apply { worldOffset = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val p = particle.loc.add(worldOffset) - val t = (particle.currentAge.toDouble() * timeScale) + phaseOffset - - // A cheap "curl-ish" analytic field - val fx = sin((p.y + t) * frequency) + cos((p.z - t) * frequency) - val fy = sin((p.z + t) * frequency) + cos((p.x + t) * frequency) - val fz = sin((p.x - t) * frequency) + cos((p.y - t) * frequency) - - // normalize-ish (avoid sqrt cost, keep stable magnitude) - val scale = 0.5 - val dv = Vec3(fx * scale, fy * scale, fz * scale).scale(amplitude) - - data.velocity = data.velocity.add(dv) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt deleted file mode 100644 index 7b73bd04..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt +++ /dev/null @@ -1,20 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.particles.ControlableParticle - -/** - * 在emitter内部使用updatePhysics 或者使用这个 - * - * @property emitter 对应粒子发射器 - */ -class ParticleGravityCommand(val emitter: ClassParticleEmitters) : ParticleCommand { - override fun execute( - data: ControlableParticleData, - particle: ControlableParticle - ) { - data.velocity = data.velocity.add(0.0, -emitter.gravity, 0.0) - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt deleted file mode 100644 index 7dd6b709..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class ParticleInheritMode { - INITIAL, - CURRENT -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt deleted file mode 100644 index 8c80e5e4..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt +++ /dev/null @@ -1,340 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import org.joml.Quaternionf -import org.joml.Vector3f -import java.util.function.Supplier -import kotlin.random.Random - -/** - * 继承速度命令(Inherit Velocity)。 - * - * 目标: - * - 让粒子继承“发射源速度”或任意自定义速度源 - * - 支持一次性继承(INITIAL)和持续跟随(CURRENT) - * - 支持按轴屏蔽、生命周期权重、空间转换、阻尼跟随 - * - * 常见场景: - * - 武器挥动拖尾:CURRENT + 适度 damping - * - 喷射火花:INITIAL + 较高 multiplier - */ -class ParticleInheritVelocityCommand() : ParticleCommand { - /** - * 速度来源。 - * - * 默认返回 `Vec3.ZERO`,通常你会传入: - * - `Supplier { emitter.emitterVelocity }` - * - 或任意自定义动态速度源 - */ - var source: Supplier = Supplier { Vec3.ZERO } - - /** - * 继承模式。 - * - * - INITIAL:只在首次执行时继承,后续保持(或按 damping 衰减) - * - CURRENT:每 tick 跟随最新来源速度 - */ - var mode: ParticleInheritMode = ParticleInheritMode.INITIAL - - /** - * 继承倍率。 - * - * - 1.0:等于来源速度 - * - <1.0:弱继承 - * - >1.0:强化继承 - */ - var multiplier: Double = 1.0 - - /** - * 轴向掩码(逐轴相乘)。 - * - * 示例: - * - (1,0,1):忽略 Y 轴继承 - * - (0,1,0):只继承竖直方向 - */ - var axisMask: Vec3 = Vec3(1.0, 1.0, 1.0) - - /** - * 生命周期权重曲线。 - * - * 采样值会乘到继承速度上: - * - 1 -> 全量继承 - * - 0 -> 不继承 - */ - var overLifetime: FloatCurve = ConstantFloatCurve(1.0) - - /** - * 阻尼强度(用于平滑/衰减继承速度)。 - * - * - 0:不平滑,立即跟随 - * - >0:指数平滑,值越大变化越柔和 - */ - var damping: Double = 0.0 - - /** - * 继承速度长度上限。 - * - * - <=0:不限制 - * - >0:超过则截断 - */ - var maxContributionSpeed: Double = 0.0 - - /** - * 继承向量所在空间。 - * - * - WORLD:按世界轴解释 - * - LOCAL:按粒子当前旋转解释后再转到世界空间 - */ - var space: ParticleMotionSpace = ParticleMotionSpace.WORLD - - /** - * 是否启用按粒子固定随机缩放。 - */ - var randomizePerParticle: Boolean = false - - /** - * 随机缩放最小值(按轴独立采样)。 - */ - var randomScaleMin: Double = 1.0 - - /** - * 随机缩放最大值(按轴独立采样)。 - */ - var randomScaleMax: Double = 1.0 - - /** - * 随机种子偏移。 - */ - var randomSeedOffset: Int = 0 - - private val stateKey = "inherit_velocity.state.${System.identityHashCode(this)}" - private val randomScaleKey = "inherit_velocity.random_scale.${System.identityHashCode(this)}" - - private data class InheritState( - var initialized: Boolean = false, - var applied: Vec3 = Vec3.ZERO - ) - - private data class AxisRandomScale( - val x: Double, - val y: Double, - val z: Double - ) - - /** - * @param source 速度来源 - * @param mode 继承模式(INITIAL/CURRENT) - * @param multiplier 继承倍率 - * @param axisMask 轴向掩码 - * @param overLifetime 生命周期权重曲线 - * @param damping 平滑阻尼 - * @param maxContributionSpeed 继承速度上限 - * @param space 继承空间(WORLD/LOCAL) - */ - constructor( - source: Supplier, - mode: ParticleInheritMode = ParticleInheritMode.INITIAL, - multiplier: Double = 1.0, - axisMask: Vec3 = Vec3(1.0, 1.0, 1.0), - overLifetime: FloatCurve = ConstantFloatCurve(1.0), - damping: Double = 0.0, - maxContributionSpeed: Double = 0.0, - space: ParticleMotionSpace = ParticleMotionSpace.WORLD - ) : this() { - this.source = source - this.mode = mode - this.multiplier = multiplier - this.axisMask = axisMask - this.overLifetime = overLifetime - this.damping = damping - this.maxContributionSpeed = maxContributionSpeed - this.space = space - } - - /** - * 便捷构造:直接继承指定发射器的每 tick 位移速度。 - */ - constructor(emitter: ClassParticleEmitters) : this( - source = Supplier { emitter.emitterVelocity } - ) - - /** - * 设置速度来源。 - */ - fun source(v: Supplier) = apply { source = v } - - /** - * 设置继承模式。 - * - * - INITIAL:一次继承 - * - CURRENT:持续跟随 - */ - fun mode(v: ParticleInheritMode) = apply { mode = v } - - /** - * 设置继承倍率。 - */ - fun multiplier(v: Double) = apply { multiplier = v } - - /** - * 设置轴向掩码。 - */ - fun axisMask(v: Vec3) = apply { axisMask = v } - - /** - * 设置生命周期权重曲线。 - */ - fun overLifetime(v: FloatCurve) = apply { overLifetime = v } - - /** - * 设置阻尼强度。 - */ - fun damping(v: Double) = apply { damping = v } - - /** - * 设置继承速度上限。 - */ - fun maxContributionSpeed(v: Double) = apply { maxContributionSpeed = v } - - /** - * 设置继承空间(WORLD/LOCAL)。 - */ - fun space(v: ParticleMotionSpace) = apply { space = v } - - /** - * 设置是否启用按粒子固定随机缩放。 - */ - fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } - - /** - * 设置随机缩放区间。 - */ - fun randomScale(min: Double, max: Double) = apply { - randomScaleMin = min - randomScaleMax = max - } - - /** - * 设置随机种子偏移。 - */ - fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } - - /** - * 每 tick 应用继承速度。 - */ - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val state = particle.controler.bufferedData.getOrPut(stateKey) { InheritState() } as InheritState - - val t = normalizedLifetime(particle) - val lifeMultiplier = overLifetime.sample(t) - val randomScale = resolveRandomScale(particle) - - var target = source.get() - target = applyMask(target) - target = scaleAxis(target, randomScale) - target = target.scale(multiplier * lifeMultiplier) - target = transformBySpace(target, space, particle) - target = clampLength(target, maxContributionSpeed) - - target = when (mode) { - ParticleInheritMode.INITIAL -> { - if (!state.initialized) { - state.initialized = true - target - } else if (damping > 0.0) { - state.applied.scale(GraphMathHelper.expDampFactor(damping, 1.0)) - } else { - state.applied - } - } - - ParticleInheritMode.CURRENT -> { - state.initialized = true - if (damping > 0.0) { - val follow = 1.0 - GraphMathHelper.expDampFactor(damping, 1.0) - state.applied.add(target.subtract(state.applied).scale(follow.coerceIn(0.0, 1.0))) - } else { - target - } - } - } - - val delta = target.subtract(state.applied) - if (delta.lengthSqr() > 1e-12) { - data.velocity = data.velocity.add(delta) - } - state.applied = target - } - - private fun applyMask(vec: Vec3): Vec3 { - return Vec3( - vec.x * axisMask.x, - vec.y * axisMask.y, - vec.z * axisMask.z - ) - } - - private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { - return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) - } - - private fun normalizedLifetime(particle: ControlableParticle): Double { - if (particle.lifetime <= 0) return 0.0 - return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) - } - - private fun transformBySpace( - vec: Vec3, - space: ParticleMotionSpace, - particle: ControlableParticle - ): Vec3 { - if (space == ParticleMotionSpace.WORLD) return vec - - val q = Quaternionf().rotateXYZ( - particle.currentPitch, - particle.currentYaw, - particle.currentRoll - ) - val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) - v.rotate(q) - return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) - } - - private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { - if (!randomizePerParticle) { - return AxisRandomScale(1.0, 1.0, 1.0) - } - val cached = particle.controler.bufferedData[randomScaleKey] - if (cached is AxisRandomScale) { - return cached - } - - val minScale = minOf(randomScaleMin, randomScaleMax) - val maxScale = maxOf(randomScaleMin, randomScaleMax) - val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) - val fixed = if (maxScale - minScale <= 1e-9) { - AxisRandomScale(minScale, minScale, minScale) - } else { - AxisRandomScale( - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale) - ) - } - particle.controler.bufferedData[randomScaleKey] = fixed - return fixed - } - - private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { - if (maxLen <= 0.0) return vec - val len = vec.length() - if (len <= 1e-9 || len <= maxLen) return vec - return vec.scale(maxLen / len) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt deleted file mode 100644 index 1fb7be06..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt +++ /dev/null @@ -1,377 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import org.joml.Quaternionf -import org.joml.Vector3f -import kotlin.random.Random - -/** - * 生命周期运动命令(Force over Lifetime + Velocity over Lifetime)。 - * - * 你可以把它理解为 Unity 里两个模块的组合版: - * - Force over Lifetime:每 tick 给 `data.velocity` 增加一段加速度 - * - Velocity over Lifetime:每 tick 按模式塑形速度(叠加/覆盖/乘法) - * - * 执行流程: - * 1. 计算生命周期进度 `t = currentAge / lifetime` - * 2. 采样 Force 曲线并叠加到当前速度 - * 3. 采样 Velocity 曲线并按 `velocityMode` 生效 - * 4. 可选:应用按粒子固定随机缩放(提升群体离散感) - */ -class ParticleLifetimeMotionCommand() : ParticleCommand { - /** - * Force 曲线 X 分量(每 tick 对 velocity.x 的增量)。 - * - * - 正值:向世界/局部 +X 推 - * - 负值:向世界/局部 -X 推 - */ - var forceX: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Force 曲线 Y 分量(每 tick 对 velocity.y 的增量)。 - * - * 常用于: - * - 上升烟雾:前期正值,后期逐渐趋近 0 - * - 下坠火星:前期小正值,后期转负值 - */ - var forceY: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Force 曲线 Z 分量(每 tick 对 velocity.z 的增量)。 - */ - var forceZ: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Velocity 曲线 X 分量。 - * - * 实际如何生效取决于 `velocityMode`: - * - ADD:视作附加速度曲线 - * - OVERRIDE:视作目标速度 - * - MULTIPLY:视作按轴倍率 - */ - var velocityX: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Velocity 曲线 Y 分量。 - */ - var velocityY: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Velocity 曲线 Z 分量。 - */ - var velocityZ: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Force 曲线所在空间。 - * - * - WORLD:曲线向量直接当世界方向 - * - LOCAL:曲线向量会按粒子当前 pitch/yaw/roll 旋转到世界方向 - */ - var forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD - - /** - * Velocity 曲线所在空间。 - * - * - WORLD:速度塑形不随粒子朝向变化 - * - LOCAL:速度塑形会跟随粒子当前朝向 - */ - var velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD - - /** - * Velocity 曲线作用模式。 - * - * - ADD:把曲线速度作为“附加项”并做增量追踪 - * - OVERRIDE:把当前速度拉向曲线速度 - * - MULTIPLY:按轴对当前速度做乘法 - */ - var velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD - - /** - * 是否启用“按粒子固定随机缩放”。 - * - * 开启后每个粒子只在首次执行时采样一次随机倍率, - * 后续 tick 重用同一倍率,保证轨迹连贯。 - */ - var randomizePerParticle: Boolean = false - - /** - * 随机缩放最小值(按轴独立采样)。 - */ - var randomScaleMin: Double = 1.0 - - /** - * 随机缩放最大值(按轴独立采样)。 - */ - var randomScaleMax: Double = 1.0 - - /** - * 随机种子偏移。 - * - * 在相同参数下改变该值,可快速得到另一套离散分布。 - */ - var randomSeedOffset: Int = 0 - - /** - * 限制 Velocity over Lifetime 在单 tick 的最大速度改变量。 - * - * - <= 0:不限制 - * - > 0:超过该值会被裁剪 - * - * 作用:抑制曲线突变导致的速度跳变或爆速。 - */ - var maxVelocityDeltaPerTick: Double = 0.0 - - private val velocityStateKey = "lifetime_motion.velocity_state.${System.identityHashCode(this)}" - private val randomScaleKey = "lifetime_motion.random_scale.${System.identityHashCode(this)}" - - private data class VelocityState(var appliedVelocity: Vec3 = Vec3.ZERO) - private data class AxisRandomScale(val x: Double, val y: Double, val z: Double) - - /** - * @param forceX Force 曲线 X - * @param forceY Force 曲线 Y - * @param forceZ Force 曲线 Z - * @param velocityX Velocity 曲线 X - * @param velocityY Velocity 曲线 Y - * @param velocityZ Velocity 曲线 Z - * @param forceSpace Force 所在空间 - * @param velocitySpace Velocity 所在空间 - * @param velocityMode Velocity 生效模式 - */ - constructor( - forceX: FloatCurve = ConstantFloatCurve(0.0), - forceY: FloatCurve = ConstantFloatCurve(0.0), - forceZ: FloatCurve = ConstantFloatCurve(0.0), - velocityX: FloatCurve = ConstantFloatCurve(0.0), - velocityY: FloatCurve = ConstantFloatCurve(0.0), - velocityZ: FloatCurve = ConstantFloatCurve(0.0), - forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, - velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, - velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD - ) : this() { - this.forceX = forceX - this.forceY = forceY - this.forceZ = forceZ - this.velocityX = velocityX - this.velocityY = velocityY - this.velocityZ = velocityZ - this.forceSpace = forceSpace - this.velocitySpace = velocitySpace - this.velocityMode = velocityMode - } - - /** - * 一次性设置 Force 曲线 XYZ。 - * - * 示例: - * - `x=0,y>0,z=0`:纯上升 - * - `x/z 对称`:横向扩散 - */ - fun forceCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { - forceX = x - forceY = y - forceZ = z - } - - /** - * 一次性设置 Velocity 曲线 XYZ。 - * - * 建议: - * - 做“轨道化速度”用 `OVERRIDE` - * - 做“微调补偿”用 `ADD` - */ - fun velocityCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { - velocityX = x - velocityY = y - velocityZ = z - } - - /** - * 设置 Force 曲线空间。 - * - * - WORLD:固定世界方向 - * - LOCAL:跟随粒子朝向 - */ - fun forceSpace(v: ParticleMotionSpace) = apply { forceSpace = v } - - /** - * 设置 Velocity 曲线空间。 - * - * - WORLD:常用于全局风场/轨道 - * - LOCAL:常用于“朝前推进”类效果 - */ - fun velocitySpace(v: ParticleMotionSpace) = apply { velocitySpace = v } - - /** - * 设置 Velocity 曲线作用模式。 - * - * - ADD / OVERRIDE / MULTIPLY - */ - fun velocityMode(v: ParticleLifetimeVelocityMode) = apply { velocityMode = v } - - /** - * 设置是否启用按粒子固定随机缩放。 - */ - fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } - - /** - * 设置随机缩放区间。 - * - * - 当 `min == max` 时等价于固定倍率 - * - 区间越大,离散感越强,但整体可控性会下降 - */ - fun randomScale(min: Double, max: Double) = apply { - randomScaleMin = min - randomScaleMax = max - } - - /** - * 设置随机种子偏移。 - */ - fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } - - /** - * 设置 Velocity 曲线每 tick 最大速度改变量。 - */ - fun maxVelocityDeltaPerTick(v: Double) = apply { maxVelocityDeltaPerTick = v } - - /** - * 每 tick 应用生命周期运动。 - */ - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val t = normalizedLifetime(particle) - val randomScale = resolveRandomScale(particle) - - val force = transformBySpace( - scaleAxis(sampleForce(t), randomScale), - forceSpace, - particle - ) - if (force.lengthSqr() > 1e-12) { - data.velocity = data.velocity.add(force) - } - - val sampledVelocity = transformBySpace( - scaleAxis(sampleVelocity(t), randomScale), - velocitySpace, - particle - ) - when (velocityMode) { - ParticleLifetimeVelocityMode.ADD -> applyVelocityAdditive(data, particle, sampledVelocity) - ParticleLifetimeVelocityMode.OVERRIDE -> { - particle.controler.bufferedData.remove(velocityStateKey) - var target = sampledVelocity - if (maxVelocityDeltaPerTick > 0.0) { - val delta = clampLength(target.subtract(data.velocity), maxVelocityDeltaPerTick) - target = data.velocity.add(delta) - } - data.velocity = target - } - - ParticleLifetimeVelocityMode.MULTIPLY -> { - particle.controler.bufferedData.remove(velocityStateKey) - val current = data.velocity - var target = Vec3( - current.x * sampledVelocity.x, - current.y * sampledVelocity.y, - current.z * sampledVelocity.z - ) - if (maxVelocityDeltaPerTick > 0.0) { - val delta = clampLength(target.subtract(current), maxVelocityDeltaPerTick) - target = current.add(delta) - } - data.velocity = target - } - } - } - - private fun applyVelocityAdditive( - data: ControlableParticleData, - particle: ControlableParticle, - sampledVelocity: Vec3 - ) { - val state = particle.controler.bufferedData.getOrPut(velocityStateKey) { VelocityState() } as VelocityState - val delta = clampLength(sampledVelocity.subtract(state.appliedVelocity), maxVelocityDeltaPerTick) - if (delta.lengthSqr() > 1e-12) { - data.velocity = data.velocity.add(delta) - } - state.appliedVelocity = state.appliedVelocity.add(delta) - } - - private fun sampleForce(t: Double): Vec3 { - return Vec3( - forceX.sample(t), - forceY.sample(t), - forceZ.sample(t) - ) - } - - private fun sampleVelocity(t: Double): Vec3 { - return Vec3( - velocityX.sample(t), - velocityY.sample(t), - velocityZ.sample(t) - ) - } - - private fun normalizedLifetime(particle: ControlableParticle): Double { - if (particle.lifetime <= 0) return 0.0 - return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) - } - - private fun transformBySpace( - vec: Vec3, - space: ParticleMotionSpace, - particle: ControlableParticle - ): Vec3 { - if (space == ParticleMotionSpace.WORLD) return vec - - val q = Quaternionf().rotateXYZ( - particle.currentPitch, - particle.currentYaw, - particle.currentRoll - ) - val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) - v.rotate(q) - return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) - } - - private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { - return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) - } - - private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { - if (!randomizePerParticle) { - return AxisRandomScale(1.0, 1.0, 1.0) - } - val cached = particle.controler.bufferedData[randomScaleKey] - if (cached is AxisRandomScale) { - return cached - } - val minScale = minOf(randomScaleMin, randomScaleMax) - val maxScale = maxOf(randomScaleMin, randomScaleMax) - val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) - val fixed = if (maxScale - minScale <= 1e-9) { - AxisRandomScale(minScale, minScale, minScale) - } else { - AxisRandomScale( - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale) - ) - } - particle.controler.bufferedData[randomScaleKey] = fixed - return fixed - } - - private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { - if (maxLen <= 0.0) return vec - val len = vec.length() - if (len <= 1e-9 || len <= maxLen) return vec - return vec.scale(maxLen / len) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt deleted file mode 100644 index c903b551..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt +++ /dev/null @@ -1,7 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class ParticleLifetimeVelocityMode { - ADD, - OVERRIDE, - MULTIPLY -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt deleted file mode 100644 index 69a4d496..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class ParticleMotionSpace { - WORLD, - LOCAL -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt deleted file mode 100644 index 9486f3fa..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt +++ /dev/null @@ -1,205 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.extend.multiply -import cn.coostack.cooparticlesapi.extend.times -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import kotlin.math.floor - -/** - * 噪声扰动(Noise / Turbulence) - * - * 作用:在每个 tick 给粒子速度叠加一股“连续的风”,让轨迹更自然、更有生命感, - * 常用于:烟雾/火焰抖动、能量流扰动、爆炸尘埃乱流、魔法粒子“活跃感”。 - * - * @property strength - * - 噪声加速度强度(每 tick 叠加到 velocity 的幅度) - * - 越大:风越“猛”,轨迹更弯、更乱、更炸;过大时会出现抽搐/抖动、偏航严重、粒子群散架。 - * - 越小:扰动更轻微,只是细腻的呼吸感/轻微摆动,整体运动仍主要由初速度决定。 - * - 经验:火焰/烟雾中等,能量微扰偏小,爆炸尘埃可偏大但必须配合 drag 或 clampSpeed。 - * - * @property frequency - * - 空间频率(噪声随“空间位置”变化的密度,决定扰动“颗粒感”) - * - 越大:噪声变化更快、更碎、更“抖”,局部细节更丰富;过大时会像高频抖动/电流乱颤。 - * - 越小:噪声变化更慢、更平滑,形成大尺度的“风带/云团”漂移;过小会显得太温柔、缺细节。 - * - 直觉:frequency 决定“风场纹理”的粗细(大=细碎,小=粗大块)。 - * - * @property speed - * - 时间滚动速度(噪声场随时间流动的快慢) - * - 越大:风场变化更快,扰动更活跃,像湍急的气流;过大时会像噪声在闪烁,产生“乱抖”的观感。 - * - 越小:风场变化更慢,像缓慢流动的空气/云层;过小会显得“静止抖动”,缺少流体感。 - * - 直觉:speed 决定“风在动得有多快”(不是风的力量,力量是 strength)。 - * - * @property affectY - * - 垂直影响比例(对 Y 方向扰动的缩放) - * - 越大:上下翻滚更明显,更“蓬/炸”,适合爆炸烟尘、魔法能量上涌;过大会显得漂浮、失重。 - * - 越小:更贴地/更平,垂直更稳定;常用于烟雾(避免上下乱跳)或贴地尘土。 - * - 常用:烟雾 0.3~0.7;能量上涌可 1.0~1.5;贴地尘土可 0.0~0.3。 - * - * @property clampSpeed - * - 限速(对最终速度做上限,防止速度叠加越来越快) - * - 越大:允许粒子跑得更快,扰动会更“飘逸/发散”;过大时可能被 strength 一直堆出很高速度。 - * - 越小:更“黏”,粒子速度被压住,群体更稳更聚;过小时会把扰动的动态感也一起压扁。 - * - 直觉:clampSpeed 是“安全阀/天花板”,用来保证效果不会失控。 - * - * @property useLifeCurve - * - 是否按生命周期调制强度(把 strength 乘上寿命曲线) - * - true:扰动会随生命周期变化(常见:出生强→死亡弱,或先弱后强),更像真实流体/能量变化。 - * - false:全程扰动强度恒定,更稳定可控,但可能显得“机械”,不够有呼吸感。 - * - 建议:大部分效果都开 true;只有想要“持续稳定噪声”时才关(例如长时间环境粒子)。 - */ -class ParticleNoiseCommand( - private var strength: Double = 0.03, - private var frequency: Double = 0.15, - private var speed: Double = 0.12, - private var affectY: Double = 1.0, - private var clampSpeed: Double = 0.8, - private var useLifeCurve: Boolean = true, -) : ParticleCommand { - constructor() : this(0.03, 0.15, 0.12, 1.0, 0.8, true) - - /** - * 设置噪声加速度强度(每 tick 叠加到 velocity 的幅度)。 - * - * - 越大:扰动更猛,轨迹更弯、更乱,需注意配合限速或阻尼 - * - 越小:扰动更轻微,仅提供细腻的呼吸感 - */ - fun strength(v: Double) = apply { strength = v } - - /** - * 设置空间频率(噪声随空间位置变化的密度)。 - * - * - 越大:扰动更细碎、更抖动 - * - 越小:扰动更平滑,形成大尺度风带 - */ - fun frequency(v: Double) = apply { frequency = v } - - /** - * 设置时间滚动速度(噪声场随时间变化的快慢)。 - * - * - 越大:风场变化更快,更活跃 - * - 越小:风场变化更慢,更沉稳 - */ - fun speed(v: Double) = apply { speed = v } - - /** - * 设置垂直方向(Y 轴)的扰动影响比例。 - * - * - 越大:上下翻滚更明显,更蓬松 - * - 越小:垂直更稳定,更贴地 - */ - fun affectY(v: Double) = apply { affectY = v } - - /** - * 设置速度上限(最终 velocity 的最大长度)。 - * - * - 用于防止噪声扰动持续叠加导致速度失控 - * - 值越大:运动更飘逸、发散 - * - 值越小:粒子群更稳、更聚 - */ - fun clampSpeed(v: Double) = apply { clampSpeed = v } - - /** - * 设置是否按生命周期曲线调制扰动强度。 - * - * - true:扰动随粒子生命周期变化,更自然、更有呼吸感 - * - false:全程恒定扰动,更稳定、可控 - */ - fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } - - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - // 0..1 生命周期进度 - val t = if (particle.lifetime > 0) particle.currentAge.toDouble() / particle.lifetime.toDouble() else 0.0 - - // 取粒子的稳定 seed(一定要稳定) - // 你可以换成 data.seed / spawnIndex / uuid hash - val seed = particle.controlUUID.hashCode() // 如果没有 id,自己做一个 - - // 时间输入:连续滚动 - val time = (particle.currentAge.toDouble()) * speed - val pos = particle.loc - // 噪声采样点:位置 * 频率 + time - val p = (Vec3(pos.x, pos.y, pos.z) * this.frequency) - .add(time, time * 0.7, time * 1.3) - - var amp = strength - if (useLifeCurve) { - // 这里你换成 data 的曲线:例如 data.strengthCurve.sample(t) - // 我给默认:前强后弱(像烟) - val lifeMul = (1.0 - t) - amp *= lifeMul - } - - val n = noiseVec3(p, seed) - val dv = Vec3(n.x, n.y * affectY, n.z).multiply(amp) - - val v0 = data.velocity - var v = v0.add(dv) - - // 限速(很重要) - val sp2 = v.lengthSqr() - val max2 = clampSpeed * clampSpeed - if (sp2 > max2) { - v = v.normalize().multiply(clampSpeed) - } - - data.velocity = v - } - - private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) - private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t - - private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { - // 纯 hash -> 0..1 - var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 - n = (n xor (n ushr 13)) * 1274126177 - n = n xor (n ushr 16) - // 转成 0..1 - return ((n and 0x7fffffff).toDouble() / 2147483647.0) - } - - private fun valueNoise3(p: Vec3, seed: Int): Double { - val x0 = floor(p.x).toInt() - val y0 = floor(p.y).toInt() - val z0 = floor(p.z).toInt() - val fx = p.x - x0 - val fy = p.y - y0 - val fz = p.z - z0 - - val u = fade(fx) - val v = fade(fy) - val w = fade(fz) - - fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) - - val n000 = h(0, 0, 0) - val n100 = h(1, 0, 0) - val n010 = h(0, 1, 0) - val n110 = h(1, 1, 0) - val n001 = h(0, 0, 1) - val n101 = h(1, 0, 1) - val n011 = h(0, 1, 1) - val n111 = h(1, 1, 1) - - val nx00 = lerp(n000, n100, u) - val nx10 = lerp(n010, n110, u) - val nx01 = lerp(n001, n101, u) - val nx11 = lerp(n011, n111, u) - - val nxy0 = lerp(nx00, nx10, v) - val nxy1 = lerp(nx01, nx11, v) - - return lerp(nxy0, nxy1, w) // 0..1 - } - - private fun noiseVec3(pos: Vec3, seed: Int): Vec3 { - // 用不同 seed 偏移得到 xyz 三通道 - val nx = valueNoise3(pos, seed + 11) * 2 - 1 - val ny = valueNoise3(pos, seed + 23) * 2 - 1 - val nz = valueNoise3(pos, seed + 37) * 2 - 1 - val v = Vec3(nx, ny, nz) - return if (v.lengthSqr() < 1e-8) Vec3.ZERO else v.normalize() - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt deleted file mode 100644 index 858fef4c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt +++ /dev/null @@ -1,177 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.max - -class ParticleOrbitCommand() : ParticleCommand { - /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 目标轨道半径(世界单位) - * - * - 值越小:轨道越紧凑 - * - 值越大:粒子绕行范围越大 - */ - var radius: Double = 3.0 - - /** - * 角速度强度(每 tick 施加的切向速度增量) - * - * - 较小:缓慢绕行 - * - 较大:快速旋转(建议配合限速或阻尼) - */ - var angularSpeed: Double = 0.35 - - /** - * 径向纠正强度(用于将粒子拉回目标半径) - * - * - 值越小:允许轨道半径有较大漂移 - * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 - */ - var radialCorrect: Double = 0.25 - - /** - * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) - */ - var minDistance: Double = 0.2 - - /** - * 轨道模式 - * - * - PHYSICAL:真实物理(默认) - * - SPRING:弹簧收敛(推荐通用) - * - SNAP:强制吸附(最稳定) - */ - var mode: OrbitMode = OrbitMode.PHYSICAL - - /** - * 仅在 PHYSICAL / SPRING 下生效: - * 每 tick 最大径向修正量(防止启动时被拉飞) - */ - var maxRadialStep: Double = 0.5 - - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - radius: Double = 3.0, - angularSpeed: Double = 0.35, - radialCorrect: Double = 0.25, - minDistance: Double = 0.2, - mode: OrbitMode = OrbitMode.PHYSICAL, - ) : this() { - this.center = center - this.axis = axis - this.radius = radius - this.angularSpeed = angularSpeed - this.radialCorrect = radialCorrect - this.minDistance = minDistance - this.mode = mode - } - - /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ - fun center(v: Supplier) = apply { center = v } - - /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ - fun axis(v: Vec3) = apply { axis = v } - - /** - * 目标轨道半径(世界单位) - * - * - 值越小:轨道越紧凑 - * - 值越大:粒子绕行范围越大 - */ - fun radius(v: Double) = apply { radius = v } - - /** - * 角速度强度(每 tick 施加的切向速度增量) - * - * - 较小:缓慢绕行 - * - 较大:快速旋转(建议配合限速或阻尼) - */ - fun angularSpeed(v: Double) = apply { angularSpeed = v } - - /** - * 径向纠正强度(用于将粒子拉回目标半径) - * - * - 值越小:允许轨道半径有较大漂移 - * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 - */ - fun radialCorrect(v: Double) = apply { radialCorrect = v } - - /** - * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) - */ - fun minDistance(v: Double) = apply { minDistance = v } - - /** - * 轨道模式 - * - * - PHYSICAL:真实物理(默认) - * - SPRING:弹簧收敛(推荐通用) - * - SNAP:强制吸附(最稳定) - */ - fun mode(v: OrbitMode) = apply { mode = v } - - /** - * 仅在 PHYSICAL / SPRING 下生效: - * 每 tick 最大径向修正量(防止启动时被拉飞) - */ - fun maxRadialStep(v: Double) = apply { maxRadialStep = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val ax = axis.normalize() - val centerPos = center.get() - - val r0 = pos.subtract(centerPos) - val axialComp = ax.scale(r0.dot(ax)) - var radial = r0.subtract(axialComp) - - var dist = radial.length() - dist = max(dist, minDistance) - - radial = radial.scale(1.0 / dist) - - // 切向方向 - var tangential = ax.cross(radial) - val tLen = tangential.length() - if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) - - val dvTan = tangential.scale(angularSpeed) - val err = dist - radius - - when (mode) { - OrbitMode.PHYSICAL -> { - val raw = -err * radialCorrect - val step = raw.coerceIn(-maxRadialStep, maxRadialStep) - val dvRad = radial.scale(step) - data.velocity = data.velocity.add(dvTan).add(dvRad) - } - - OrbitMode.SPRING -> { - // 二阶感:误差越大,拉力越强,但更温和 - val spring = -err * radialCorrect - val step = spring.coerceIn(-maxRadialStep, maxRadialStep) - val dvRad = radial.scale(step) - data.velocity = data.velocity.add(dvTan).add(dvRad) - } - - OrbitMode.SNAP -> { - // 直接把粒子“吸附”到目标半径 - val targetPos = centerPos - .add(axialComp) - .add(radial.scale(radius)) - - val snapVec = targetPos.subtract(pos) - data.velocity = data.velocity.add(dvTan).add(snapVec.scale(radialCorrect)) - } - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt deleted file mode 100644 index 29e483e5..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt +++ /dev/null @@ -1,98 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier - -/** - * 旋转力:让粒子绕某个轴产生切向速度(像被“搅拌”)。 - * - * 典型效果: - * - 火焰上升的旋涡感 - * - 魔法阵环绕 - * - 爆炸烟雾“打旋” - */ -class ParticleRotationForceCommand() : ParticleCommand { - - /** 旋转中心(世界坐标)。 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** - * 旋转轴(会被自动 normalize)。 - * - * - (0,1,0) 绕 Y 轴旋转(最常用) - * - (1,0,0) 绕 X - * - (0,0,1) 绕 Z - */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 旋转强度(每 tick 切向速度增量)。 - * - * - 0.05 ~ 0.3:轻微旋 - * - 0.3 ~ 2.0:明显旋 - * - >2.0:非常暴力(建议配合阻尼/限速) - */ - var strength: Double = 0.35 - - /** - * 距离衰减范围(>0)。 - * - * - 越大:远处仍然能被带动旋转 - * - 越小:只在中心附近打旋 - */ - var range: Double = 8.0 - - /** 衰减幂次(>=1)。 */ - var falloffPower: Double = 2.0 - - /** - * @param center 旋转中心 - * @param axis 旋转轴(自动 normalize) - * @param strength 旋转强度(每 tick 切向速度增量) - * @param range 衰减范围尺度 - * @param falloffPower 衰减幂次(>=1) - */ - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - strength: Double = 0.35, - range: Double = 8.0, - falloffPower: Double = 2.0, - ) : this() { - this.center = center - this.axis = axis - this.strength = strength - this.range = range - this.falloffPower = falloffPower - } - - fun center(v: Supplier) = apply { - center = v - } - fun axis(v: Vec3) = apply { axis = v } - fun strength(v: Double) = apply { strength = v } - fun range(v: Double) = apply { range = v } - fun falloffPower(v: Double) = apply { falloffPower = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val r = pos.subtract(center.get()) - val dist = r.length() - if (dist < 1e-9) return - - val ax = axis.normalize() - // tangential direction: axis x r - var t = ax.cross(r) - val tLen = t.length() - if (tLen < 1e-9) return - t = t.scale(1.0 / tLen) - - val falloff = GraphMathHelper.inversePowerFalloff(dist, range, falloffPower) - val dv = t.scale(strength * falloff) - - data.velocity = data.velocity.add(dv) - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt deleted file mode 100644 index 69a591f2..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt +++ /dev/null @@ -1,226 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.extend.plus -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.abs -import kotlin.math.sqrt - -/** - * 环面回流场。 - * - * 在一圈局部区域里制造“翻卷”轨迹,适合蘑菇云帽檐、烟团边缘这类效果。 - * 它会优先“扭转当前速度方向”,而不是把粒子硬拽成一个环, - * 所以在有阻尼/减速时,卷动也会跟着自然变慢。 - * 它改的是粒子速度,不是 billboard 朝向。 - */ -class ParticleToroidalCirculationCommand : ParticleCommand { - /** 环流中心。做蘑菇云时一般填帽子中心。 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** 主轴,默认 Y 轴。 */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** 主半径,也就是翻卷带离中心的距离。 */ - var ringRadius: Double = 3.0 - - /** 径向厚度。 */ - var radialThickness: Double = 1.2 - - /** 轴向厚度。 */ - var axialThickness: Double = 0.8 - - /** 翻卷力度,负数表示反向。 */ - var circulationStrength: Double = 0.35 - - /** 向外撑开的附加力度。 */ - var outwardStrength: Double = 0.0 - - /** 向上抬的附加力度。 */ - var upwardStrength: Double = 0.0 - - /** 往翻卷带回带/导向的力度。主要用于带外把速度重新导回卷动区。 */ - var followStrength: Double = 0.12 - - /** 单 tick 最大修正量。<=0 不限制。 */ - var maxStep: Double = 0.6 - - /** 是否按生命周期减弱。 */ - var useLifeCurve: Boolean = false - - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - ringRadius: Double = 3.0, - radialThickness: Double = 1.2, - axialThickness: Double = 0.8, - circulationStrength: Double = 0.35, - outwardStrength: Double = 0.0, - upwardStrength: Double = 0.0, - followStrength: Double = 0.12, - maxStep: Double = 0.6, - useLifeCurve: Boolean = false, - ) { - this.center = center - this.axis = axis - this.ringRadius = ringRadius - this.radialThickness = radialThickness - this.axialThickness = axialThickness - this.circulationStrength = circulationStrength - this.outwardStrength = outwardStrength - this.upwardStrength = upwardStrength - this.followStrength = followStrength - this.maxStep = maxStep - this.useLifeCurve = useLifeCurve - } - - fun center(v: Supplier) = apply { center = v } - - fun center(v: Vec3) = apply { center = Supplier { v } } - - fun center(x: Double, y: Double, z: Double) = apply { center = Supplier { Vec3(x, y, z) } } - - fun axis(v: Vec3) = apply { axis = v } - - fun axis(x: Double, y: Double, z: Double) = apply { axis = Vec3(x, y, z) } - - fun ringRadius(v: Double) = apply { ringRadius = v } - - fun radialThickness(v: Double) = apply { radialThickness = v } - - fun axialThickness(v: Double) = apply { axialThickness = v } - - /** 同时设置两个厚度,方便网页只留一个输入框。 */ - fun thickness(v: Double) = apply { - radialThickness = v - axialThickness = v - } - - fun circulationStrength(v: Double) = apply { circulationStrength = v } - - - fun outwardStrength(v: Double) = apply { outwardStrength = v } - - fun upwardStrength(v: Double) = apply { upwardStrength = v } - - fun followStrength(v: Double) = apply { followStrength = v } - - fun maxStep(v: Double) = apply { maxStep = v } - - fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val centerPos = center.get() - val ax = safeNormalize(axis) - val pos = particle.loc - val rel = pos.subtract(centerPos) - - val axialDistance = rel.dot(ax) - val planar = rel.subtract(ax.scale(axialDistance)) - val planarLen = planar.length() - - val radialDir = if (planarLen < 1e-9) { - anyPerp(ax) - } else { - planar.scale(1.0 / planarLen) - } - - val qr = planarLen - ringRadius - - val radialSize = radialThickness.coerceAtLeast(1e-6) - val axialSize = axialThickness.coerceAtLeast(1e-6) - - val localRadial = qr / radialSize - val localAxial = axialDistance / axialSize - val normalizedDistance = sqrt(localRadial * localRadial + localAxial * localAxial) - - val insideBand = normalizedDistance < 1.0 - val bandWeight = if (insideBand) smooth01(1.0 - normalizedDistance) else 0.0 - val outerDistance = (normalizedDistance - 1.0).coerceAtLeast(0.0) - val captureWeight = if (outerDistance > 0.0) { - 1.0 / (1.0 + outerDistance * outerDistance * 4.0) - } else { - 0.0 - } - val lifeMul = if (useLifeCurve && particle.lifetime > 0) { - (1.0 - particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) - } else { - 1.0 - } - if (lifeMul <= 1e-9) { - return - } - - val currentVelocity = if (data.velocity.lengthSqr() > 1e-12) data.velocity else particle.velocity - val currentSpeed = currentVelocity.length() - if (currentSpeed <= 1e-9) { - return - } - val currentDir = currentVelocity.scale(1.0 / currentSpeed) - - val circulationVector = radialDir.scale(-localAxial) - .add(ax.scale(localRadial)) - val toBandCenter = radialDir.scale(-localRadial).add(ax.scale(-localAxial)) - - var desiredFlow = Vec3.ZERO - - val circulationWeight = bandWeight * lifeMul - if (circulationStrength != 0.0 && circulationWeight > 1e-9 && circulationVector.lengthSqr() > 1e-12) { - desiredFlow = desiredFlow.add(circulationVector.scale(circulationStrength * circulationWeight)) - } - if (insideBand && outwardStrength != 0.0 && circulationWeight > 1e-9) { - desiredFlow = desiredFlow.add(radialDir.scale(outwardStrength * circulationWeight)) - } - if (insideBand && upwardStrength != 0.0 && circulationWeight > 1e-9) { - desiredFlow = desiredFlow.add(ax.scale(upwardStrength * circulationWeight)) - } - - val captureMul = captureWeight * lifeMul - if (followStrength != 0.0 && captureMul > 1e-9 && toBandCenter.lengthSqr() > 1e-12) { - desiredFlow = desiredFlow.add(normalizeOrZero(toBandCenter).scale(followStrength * captureMul)) - } - - val desiredFlowLen = desiredFlow.length() - if (desiredFlowLen <= 1e-9) { - return - } - val desiredDir = desiredFlow.scale(1.0 / desiredFlowLen) - val turnedDir = normalizeOrZero(currentDir.add(desiredDir.scale(desiredFlowLen))) - if (turnedDir.lengthSqr() <= 1e-12) { - return - } - - var dv = turnedDir.scale(currentSpeed).subtract(currentVelocity) - - val dvLen = dv.length() - if (maxStep > 0.0 && dvLen > maxStep) { - dv = dv.scale(maxStep / dvLen) - } - - data.velocity += dv - } - - private fun safeNormalize(v: Vec3): Vec3 { - val len = v.length() - return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) - } - - private fun normalizeOrZero(v: Vec3): Vec3 { - val len = v.length() - return if (len < 1e-9) Vec3.ZERO else v.scale(1.0 / len) - } - - private fun anyPerp(axis: Vec3): Vec3 { - val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) - val perp = axis.cross(ref) - val len = perp.length() - return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) - } - - private fun smooth01(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - return clamped * clamped * (3.0 - 2.0 * clamped) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt deleted file mode 100644 index 4c86af8d..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt +++ /dev/null @@ -1,161 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.max - -/** - * 漩涡:让粒子围绕轴旋转,并可向轴线“吸入”,以及沿轴“上升/下沉”。 - * - * 常用组合: - * - swirlStrength > 0 : 打旋 - * - radialPull > 0 : 往轴线收拢(像龙卷风) - * - axialLift > 0 : 沿轴向上(像火焰/风柱) - */ -class ParticleVortexCommand() : ParticleCommand { - - /** 漩涡中心点(轴线穿过此点)。 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** 轴方向(自动 normalize)。 */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 切向旋转强度(每 tick 切向速度增量) - * - * - 0.1 ~ 0.8:柔和漩涡 - * - 0.8 ~ 3.0:明显龙卷感 - */ - var swirlStrength: Double = 0.8 - - /** - * 向轴线吸入的强度(每 tick 径向速度增量) - * - * - 0:不收拢(只打旋) - * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” - */ - var radialPull: Double = 0.35 - - /** - * 沿轴方向的升力(每 tick 轴向速度增量) - * - * - >0:沿轴正方向上升 - * - <0:沿轴反方向下沉 - */ - var axialLift: Double = 0.0 - - /** 衰减范围尺度(>0)。 */ - var range: Double = 10.0 - - /** 衰减幂次(>=1)。 */ - var falloffPower: Double = 2.0 - - /** 避免 r=0 的最小距离钳制 */ - var minDistance: Double = 0.2 - - /** - * @param center 漩涡中心(轴线穿过点) - * @param axis 轴方向(自动 normalize) - * @param swirlStrength 旋转强度(切向) - * @param radialPull 径向吸入强度 - * @param axialLift 轴向升力 - * @param range 衰减范围尺度 - * @param falloffPower 衰减幂次 - * @param minDistance 最小距离钳制,避免抖动/爆炸 - */ - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - swirlStrength: Double = 0.8, - radialPull: Double = 0.35, - axialLift: Double = 0.0, - range: Double = 10.0, - falloffPower: Double = 2.0, - minDistance: Double = 0.2, - ) : this() { - this.center = center - this.axis = axis - this.swirlStrength = swirlStrength - this.radialPull = radialPull - this.axialLift = axialLift - this.range = range - this.falloffPower = falloffPower - this.minDistance = minDistance - } - - /** 漩涡中心点(轴线穿过此点)。 */ - fun center(v: Supplier) = apply { center = v } - - /** 轴方向(自动 normalize)。 */ - fun axis(v: Vec3) = apply { axis = v } - - /** - * 切向旋转强度(每 tick 切向速度增量) - * - * - 0.1 ~ 0.8:柔和漩涡 - * - 0.8 ~ 3.0:明显龙卷感 - */ - fun swirlStrength(v: Double) = apply { swirlStrength = v } - - /** - * 向轴线吸入的强度(每 tick 径向速度增量) - * - * - 0:不收拢(只打旋) - * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” - */ - fun radialPull(v: Double) = apply { radialPull = v } - - /** - * 沿轴方向的升力(每 tick 轴向速度增量) - * - * - >0:沿轴正方向上升 - * - <0:沿轴反方向下沉 - */ - fun axialLift(v: Double) = apply { axialLift = v } - - /** 衰减范围尺度(>0)。 */ - fun range(v: Double) = apply { range = v } - - /** 衰减幂次(>=1)。 */ - fun falloffPower(v: Double) = apply { falloffPower = v } - - /** 避免 r=0 的最小距离钳制 */ - fun minDistance(v: Double) = apply { minDistance = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val ax = axis.normalize() - - // vector from center - val r = pos.subtract(center.get()) - - // remove axial component -> radial vector to axis line - val axialComp = ax.scale(r.dot(ax)) - val radial = r.subtract(axialComp) - - var dist = radial.length() - if (dist < 1e-9) dist = 0.0 - - val d = max(dist, minDistance) - val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) - - // tangential direction - var tangential = ax.cross(radial) - val tLen = tangential.length() - if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) - - // radial inward direction - var inward = Vec3.ZERO - val rLen = radial.length() - if (rLen > 1e-9) inward = radial.scale(-1.0 / rLen) - - val dv = tangential.scale(swirlStrength * falloff) - .add(inward.scale(radialPull * falloff)) - .add(ax.scale(axialLift * falloff)) - - data.velocity = data.velocity.add(dv) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt deleted file mode 100644 index 618855d8..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt +++ /dev/null @@ -1,10 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -data class BezierFloatKeyframe( - val time: Double, - val value: Double, - val outX: Double = 0.0, - val outY: Double = 0.0, - val inX: Double = 0.0, - val inY: Double = 0.0 -) diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt deleted file mode 100644 index b8838ff9..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt +++ /dev/null @@ -1,250 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -import kotlin.math.abs -import kotlin.math.sqrt - -class BezierKeyframeFloatCurve( - frames: List = listOf(BezierFloatKeyframe(0.0, 0.0)) -) : FloatCurve { - private val keyframes: MutableList = - frames.map { it.withClampedTime() } - .sortedBy { it.time } - .toMutableList() - - init { - if (keyframes.isEmpty()) { - keyframes.add(BezierFloatKeyframe(0.0, 0.0)) - } - } - - fun setFrames(frames: List) = apply { - keyframes.clear() - keyframes.addAll( - frames.map { it.withClampedTime() } - .sortedBy { it.time } - ) - if (keyframes.isEmpty()) { - keyframes.add(BezierFloatKeyframe(0.0, 0.0)) - } - } - - fun addFrame( - time: Double, - value: Double, - outX: Double = 0.0, - outY: Double = 0.0, - inX: Double = 0.0, - inY: Double = 0.0 - ) = apply { - keyframes.add( - BezierFloatKeyframe( - time = time.coerceIn(0.0, 1.0), - value = value, - outX = outX, - outY = outY, - inX = inX, - inY = inY - ) - ) - keyframes.sortBy { it.time } - } - - /** - * 返回当前锚点和控制柄的不可变快照。 - * - * 示例:`CParticleCurve.fromFloatCurve(curve)` 使用此快照保留 GPU 控制柄。 - * 禁止:修改返回列表不会改变此曲线;需要修改时应调用 [setFrames]。 - * - * @return 按时间升序排列的当前关键帧 - */ - fun frames(): List = keyframes.toList() - - override fun sample(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - if (keyframes.size == 1) { - return keyframes[0].value - } - if (clamped <= keyframes.first().time) { - return keyframes.first().value - } - if (clamped >= keyframes.last().time) { - return keyframes.last().value - } - - for (i in 1 until keyframes.size) { - val prev = keyframes[i - 1] - val next = keyframes[i] - if (clamped <= next.time) { - return sampleSegment(prev, next, clamped) - } - } - return keyframes.last().value - } - - private fun sampleSegment(prev: BezierFloatKeyframe, next: BezierFloatKeyframe, time: Double): Double { - val span = next.time - prev.time - if (span <= EPSILON) { - return next.value - } - - val p0x = prev.time - val p1x = prev.time + prev.outX / 100.0 - val p2x = next.time + next.inX / 100.0 - val p3x = next.time - - val p0y = prev.value - val p1y = prev.value + prev.outY - val p2y = next.value + next.inY - val p3y = next.value - - val linearGuess = ((time - prev.time) / span).coerceIn(0.0, 1.0) - val u = solveParameterForX(time, p0x, p1x, p2x, p3x, linearGuess) - return cubicBezier(u, p0y, p1y, p2y, p3y) - } - - private fun solveParameterForX( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double, - guess: Double - ): Double { - if (p0 <= p1 && p1 <= p2 && p2 <= p3) { - return solveMonotonicParameter(targetX, p0, p1, p2, p3) - } - - val roots = mutableListOf() - val splitPoints = derivativeRoots(p0, p1, p2, p3) - .filter { it > EPSILON && it < 1.0 - EPSILON } - .plus(0.0) - .plus(1.0) - .sorted() - - for (i in 1 until splitPoints.size) { - val lo = splitPoints[i - 1] - val hi = splitPoints[i] - val flo = cubicBezier(lo, p0, p1, p2, p3) - targetX - val fhi = cubicBezier(hi, p0, p1, p2, p3) - targetX - when { - abs(flo) <= EPSILON -> roots.add(lo) - abs(fhi) <= EPSILON -> roots.add(hi) - flo < 0.0 && fhi > 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) - flo > 0.0 && fhi < 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) - } - } - - return roots.distinctBy { (it / EPSILON).toLong() } - .minByOrNull { abs(it - guess) } - ?: closestSampledParameter(targetX, p0, p1, p2, p3, guess) - } - - private fun solveMonotonicParameter( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double - ): Double { - var lo = 0.0 - var hi = 1.0 - var mid = 0.5 - repeat(32) { - mid = (lo + hi) * 0.5 - if (cubicBezier(mid, p0, p1, p2, p3) < targetX) { - lo = mid - } else { - hi = mid - } - } - return mid - } - - private fun solveBracketedParameter( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double, - startLo: Double, - startHi: Double - ): Double { - var lo = startLo - var hi = startHi - var mid = (lo + hi) * 0.5 - val increasing = cubicBezier(hi, p0, p1, p2, p3) >= cubicBezier(lo, p0, p1, p2, p3) - repeat(32) { - mid = (lo + hi) * 0.5 - val x = cubicBezier(mid, p0, p1, p2, p3) - if ((x < targetX) == increasing) { - lo = mid - } else { - hi = mid - } - } - return mid - } - - private fun derivativeRoots(p0: Double, p1: Double, p2: Double, p3: Double): List { - val a = -p0 + 3.0 * p1 - 3.0 * p2 + p3 - val b = 3.0 * p0 - 6.0 * p1 + 3.0 * p2 - val c = -3.0 * p0 + 3.0 * p1 - - val qa = 3.0 * a - val qb = 2.0 * b - val qc = c - if (abs(qa) <= EPSILON) { - return if (abs(qb) <= EPSILON) emptyList() else listOf(-qc / qb) - } - - val discriminant = qb * qb - 4.0 * qa * qc - if (discriminant < 0.0) { - return emptyList() - } - - val root = sqrt(discriminant) - return listOf( - (-qb - root) / (2.0 * qa), - (-qb + root) / (2.0 * qa) - ) - } - - private fun closestSampledParameter( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double, - guess: Double - ): Double { - var best = guess - var bestError = abs(cubicBezier(guess, p0, p1, p2, p3) - targetX) - for (i in 0..64) { - val u = i / 64.0 - val error = abs(cubicBezier(u, p0, p1, p2, p3) - targetX) - if (error < bestError) { - best = u - bestError = error - } - } - return best - } - - private fun cubicBezier(t: Double, p0: Double, p1: Double, p2: Double, p3: Double): Double { - val u = 1.0 - t - val u2 = u * u - val t2 = t * t - return u2 * u * p0 + - 3.0 * u2 * t * p1 + - 3.0 * u * t2 * p2 + - t2 * t * p3 - } - - private fun BezierFloatKeyframe.withClampedTime(): BezierFloatKeyframe { - return copy(time = time.coerceIn(0.0, 1.0)) - } - - private companion object { - const val EPSILON = 1.0E-9 - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt deleted file mode 100644 index 2717be1f..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt +++ /dev/null @@ -1,18 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -/** - * 常量曲线:全生命周期输出同一个值。 - * - * 典型用途: - * - 固定重力倍率 - * - 固定速度修正 - * - 作为其它更复杂曲线的默认占位 - */ -class ConstantFloatCurve( - /** - * 常量值。 - */ - var value: Double = 0.0 -) : FloatCurve { - override fun sample(t: Double): Double = value -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt deleted file mode 100644 index 2998ffa8..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt +++ /dev/null @@ -1,22 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -/** - * 生命周期曲线接口(标量)。 - * - * 约定: - * - 输入 `t` 是生命周期进度,通常期望在 `[0,1]` 范围内 - * - 实现类应尽量在内部做 `t` 的钳制,避免调用方传入异常值时不稳定 - * - * 用途: - * - 给粒子命令(如 Force/Velocity/Inherit)提供“随时间变化”的系数 - * - 统一不同曲线实现(常量、线性、关键帧)的采样入口 - */ -fun interface FloatCurve { - /** - * 采样曲线值。 - * - * @param t 生命周期进度(通常是 `currentAge / lifetime`) - * @return 曲线在该进度下的值 - */ - fun sample(t: Double): Double -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt deleted file mode 100644 index d8953cf5..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt +++ /dev/null @@ -1,12 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -/** - * 标量关键帧。 - * - * @property time 关键帧时间点,建议使用 `[0,1]` 的生命周期归一化时间 - * @property value 关键帧数值 - */ -data class FloatKeyframe( - val time: Double, - val value: Double -) diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt deleted file mode 100644 index 56253661..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt +++ /dev/null @@ -1,78 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -import cn.coostack.cooparticlesapi.utils.GraphMathHelper - -/** - * 关键帧曲线(线性插值)。 - * - * 规则: - * - 帧会按 `time` 升序保存 - * - `t` 落在相邻关键帧之间时,使用线性插值 - * - `t` 小于首帧或大于尾帧时,返回边界帧值 - */ -class KeyframeFloatCurve( - frames: List = listOf(FloatKeyframe(0.0, 0.0)) -) : FloatCurve { - private val keyframes: MutableList = - frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } - .sortedBy { it.time } - .toMutableList() - - init { - if (keyframes.isEmpty()) { - keyframes.add(FloatKeyframe(0.0, 0.0)) - } - } - - /** - * 批量替换关键帧列表。 - * - * - 时间会被钳制到 `[0,1]` - * - 内部会自动按时间升序排序 - */ - fun setFrames(frames: List) = apply { - keyframes.clear() - keyframes.addAll( - frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } - .sortedBy { it.time } - ) - if (keyframes.isEmpty()) { - keyframes.add(FloatKeyframe(0.0, 0.0)) - } - } - - /** - * 追加一个关键帧。 - * - * @param time 帧时间(会钳制到 `[0,1]`) - * @param value 帧值 - */ - fun addFrame(time: Double, value: Double) = apply { - keyframes.add(FloatKeyframe(time.coerceIn(0.0, 1.0), value)) - keyframes.sortBy { it.time } - } - - override fun sample(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - if (keyframes.size == 1) { - return keyframes[0].value - } - if (clamped <= keyframes.first().time) { - return keyframes.first().value - } - if (clamped >= keyframes.last().time) { - return keyframes.last().value - } - - for (i in 1 until keyframes.size) { - val prev = keyframes[i - 1] - val next = keyframes[i] - if (clamped <= next.time) { - val seg = (next.time - prev.time).coerceAtLeast(1e-9) - val alpha = (clamped - prev.time) / seg - return GraphMathHelper.lerp(alpha, prev.value, next.value) - } - } - return keyframes.last().value - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt deleted file mode 100644 index e11c75e5..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt +++ /dev/null @@ -1,27 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -import cn.coostack.cooparticlesapi.utils.GraphMathHelper - -/** - * 线性曲线:在 `[0,1]` 区间内从 `from` 匀速过渡到 `to`。 - * - * 典型用途: - * - 生命周期内“线性增强/衰减” - * - 先快速验证一个效果趋势,再换成关键帧曲线精调 - */ -class LinearFloatCurve( - /** - * `t=0` 时的值。 - */ - var from: Double = 0.0, - - /** - * `t=1` 时的值。 - */ - var to: Double = 1.0 -) : FloatCurve { - override fun sample(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - return GraphMathHelper.lerp(clamped, from, to) - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt deleted file mode 100644 index f3077345..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BallWindDirection.kt +++ /dev/null @@ -1,77 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind - -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import com.ezylang.evalex.Expression -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 - -class BallWindDirection( - override var direction: Vec3, - var radius: Double, - var offset: RelativeLocation, -) : WindDirection { - override var relative: Boolean = false - override var windSpeedExpress: String = "1" - override fun loadEmitters(emitters: ParticleEmitters): WindDirection { - this.emitters = emitters - return this - } - - override fun hasLoadedEmitters(): Boolean { - return emitters != null - } - - private var emitters: ParticleEmitters? = null - - companion object { - @JvmStatic - val CODEC = StreamCodec.of( - { buf, data -> - data as BallWindDirection - buf.writeVec3(data.direction) - buf.writeBoolean(data.relative) - buf.writeUtf(data.windSpeedExpress) - buf.writeVec3(data.offset.toVector()) - buf.writeDouble(data.radius) - }, { - val direction = it.readVec3() - val relative = it.readBoolean() - val express = it.readUtf() - val offset = RelativeLocation.of(it.readVec3()) - val radius = it.readDouble() - BallWindDirection(direction, radius, offset).apply { - this.relative = relative - this.windSpeedExpress = express - } - } - ) - const val ID = "ball" - } - - override fun getID(): String { - return ID - } - - override fun getWind(particlePos: Vec3): Vec3 { - if (relative) { - val pos = emitters?.pos ?: return direction - val dir = particlePos.subtract(pos) - val express = Expression(windSpeedExpress) - .with("l", dir.length()) - .evaluate().numberValue.toDouble() - dir.normalize().scale(express) - return dir - } - return direction - } - - override fun inRange(pos: Vec3): Boolean { - return emitters!!.pos.add(offset.toVector()).subtract(pos).length() <= radius - } - - override fun getCodec(): StreamCodec { - return CODEC - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt deleted file mode 100644 index 18703ca0..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/BoxWindDirection.kt +++ /dev/null @@ -1,92 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind - -import cn.coostack.cooparticlesapi.barrages.HitBox -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import com.ezylang.evalex.Expression -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 -import kotlin.math.exp - -class BoxWindDirection( - override var direction: Vec3, - var box: HitBox, - var offset: RelativeLocation, -) : WindDirection { - override var relative: Boolean = false - override var windSpeedExpress: String = "1" - override fun loadEmitters(emitters: ParticleEmitters): WindDirection { - this.emitters = emitters - return this - } - - private var emitters: ParticleEmitters? = null - - companion object { - @JvmStatic - val CODEC = StreamCodec.of( - { buf, data -> - data as BoxWindDirection - buf.writeVec3(data.direction) - buf.writeBoolean(data.relative) - buf.writeUtf(data.windSpeedExpress) - buf.writeVec3(data.offset.toVector()) - buf.writeDouble(data.box.x1) - buf.writeDouble(data.box.x2) - buf.writeDouble(data.box.y1) - buf.writeDouble(data.box.y2) - buf.writeDouble(data.box.z1) - buf.writeDouble(data.box.z2) - }, { - val direction = it.readVec3() - val relative = it.readBoolean() - val express = it.readUtf() - val offset = RelativeLocation.of(it.readVec3()) - val box = HitBox( - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - ) - BoxWindDirection(direction, box, offset).apply { - this.relative = relative - this.windSpeedExpress = express - } - } - ) - const val ID = "box" - } - - override fun getID(): String { - return ID - } - - override fun hasLoadedEmitters(): Boolean { - return emitters != null - } - - override fun getWind(particlePos: Vec3): Vec3 { - if (relative) { - val pos = emitters?.pos ?: return direction - val dir = particlePos.subtract(pos) - val express = Expression(windSpeedExpress) - .with("l", dir.length()) - .evaluate().numberValue.toDouble() - dir.normalize().scale(express) - return dir - } - return direction - } - - override fun inRange(pos: Vec3): Boolean { - val ofBox = box.ofBox(emitters!!.pos.add(offset.toVector())) - return ofBox.contains(pos) - } - - override fun getCodec(): StreamCodec { - return CODEC - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt deleted file mode 100644 index 2f97c8ae..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/GlobalWindDirection.kt +++ /dev/null @@ -1,68 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind - -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import com.ezylang.evalex.Expression -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 -import kotlin.math.exp - -class GlobalWindDirection( - override var direction: Vec3 -) : WindDirection { - override var relative: Boolean = false - override var windSpeedExpress: String = "1" - override fun loadEmitters(emitters: ParticleEmitters): WindDirection { - this.emitters = emitters - return this - } - - private var emitters: ParticleEmitters? = null - - companion object { - @JvmStatic - val CODEC = StreamCodec.of( - { buf, data -> - buf.writeVec3(data.direction) - buf.writeBoolean(data.relative) - buf.writeUtf(data.windSpeedExpress) - }, { - val direction = it.readVec3() - val relative = it.readBoolean() - val express = it.readUtf() - GlobalWindDirection(direction).apply { - this.relative = relative - this.windSpeedExpress = express - } - } - ) - const val ID = "global" - } - - override fun getID(): String { - return ID - } - override fun hasLoadedEmitters(): Boolean { - return emitters != null - } - override fun getWind(particlePos: Vec3): Vec3 { - if (relative) { - val pos = emitters?.pos ?: return direction - val dir = particlePos.subtract(pos) - val express = Expression(windSpeedExpress) - .with("l", dir.length()) - .evaluate().numberValue.toDouble() - dir.normalize().scale(express) - return dir - } - return direction - } - - override fun inRange(pos: Vec3): Boolean { - return true - } - - override fun getCodec(): StreamCodec { - return CODEC - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt deleted file mode 100644 index d080bd33..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirection.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind - -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 - -interface WindDirection { - /** - * 风向 - * 如果relative 为true 则此选项作废 - * direction.length() 代表风速大小 - * 如果设置为0向量 则代表没有风 - * relative为true 时代表粒子相对发射器位置为风向 - */ - var direction: Vec3 - var relative: Boolean - - /** - * relative 为false时此功能不启用 - * 若relative为true - * 则此参数代表风力大小 - * 表达式采用EvalEx3.5.0 - * 提供的变量 l (代表粒子与发射器的实际距离) - * 返回值是风速大小(direction.length()) - */ - var windSpeedExpress: String - - fun loadEmitters(emitters: ParticleEmitters): WindDirection - - fun hasLoadedEmitters(): Boolean - - fun getID(): String - - /** - * 获得当前位置的风速向量 - */ - fun getWind(particlePos: Vec3): Vec3 - - /** - * 粒子位置是否在可操作范围内 - */ - fun inRange(pos: Vec3): Boolean - - fun getCodec(): StreamCodec -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt deleted file mode 100644 index 59c65a77..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/environment/wind/WindDirections.kt +++ /dev/null @@ -1,57 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind - -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 - -object WindDirections { - private val packets = HashMap>() - - val GLOBAL = register("global", GlobalWindDirection.CODEC) - - val BOX = register("box", BoxWindDirection.CODEC) - - val BALL = register("ball", BallWindDirection.CODEC) - - /** - * 物理处理风力 - * @param v 先前移动方向 - * @param pos 在风场中的位置 (在粒子发射器中就是粒子位置) 如果超出范围则会返回0向量 - */ - fun handleWindForce( - wind: WindDirection, - pos: Vec3, - airDensity: Double, - dragCoefficient: Double, - crossSectionalArea: Double, - v: Vec3 = Vec3.ZERO, - ): Vec3 { - if (!wind.inRange(pos)) { - return Vec3.ZERO - } - val windVec = wind.getWind(pos) - return if (windVec.lengthSqr() > 0) { - val relativeWind = windVec.subtract(v) - val windMagnitude = 0.5 * airDensity * dragCoefficient * - crossSectionalArea * relativeWind.lengthSqr() * 0.05 - relativeWind.normalize().scale(windMagnitude) - } else { - Vec3.ZERO - } - } - - fun getCodecFromID(id: String): StreamCodec { - return packets[id]!! - } - - fun register( - id: String, - codec: StreamCodec - ): StreamCodec { - packets[id] = codec - return codec - } - - fun init() {} -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt deleted file mode 100644 index 0ed4fbff..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.BlockHitResult - -/** - * 粒子碰撞方块事件 - * 用于复用物理粒子 - */ -class ParticleCollideEvent( - override var particle: ControlableParticle, - override var particleData: ControlableParticleData, - var res: BlockHitResult -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleColliderEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt deleted file mode 100644 index 9d7a3cad..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.particles.ControlableParticle - -interface ParticleEvent { - /** - * 触发事件的粒子 - */ - var particle: ControlableParticle - var particleData: ControlableParticleData - - /** - * 取消接下来的粒子事件队列 - */ - var canceled: Boolean - - /** - * 用于方便存储 - * 获取事件ID - */ - fun getEventID(): String -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt deleted file mode 100644 index e6a1b67b..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt +++ /dev/null @@ -1,26 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -/** - * 自动注册 - * 事件处理器是单例模式 - */ -interface ParticleEventHandler : Comparable { - - fun handle(event: ParticleEvent) - - /** - * 详细见 - * 目标事件的EventID - * @see ParticleEvent.getEventID - */ - fun getTargetEventID(): String - - fun getHandlerID(): String - - fun getPriority(): Int - - override fun compareTo(other: ParticleEventHandler): Int { - return getPriority() - other.getPriority() - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt deleted file mode 100644 index 7a2a4446..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt +++ /dev/null @@ -1,56 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.reflect.CooAPIScanner -import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo -import cn.coostack.cooparticlesapi.utils.ReflectUtil -import java.lang.reflect.Modifier - -object ParticleEventHandlerManager { - private val registerHandlers = HashMap() - - fun getHandlerById(id: String): ParticleEventHandler? { - return registerHandlers[id] - } - - fun register(event: ParticleEventHandler) { - registerHandlers[event.getHandlerID()] = event - } - - fun hasRegister(id: String): Boolean = getHandlerById(id) != null - - private var handled = false - fun registerScanner() { - if (handled) { - return - } - val before = registerHandlers.size - CooParticlesConstants.logger.info("正在自动注册 ParticleEventHandler") - var count = 0 - val start = System.currentTimeMillis() - CooAPIScanner.getWithAnnotation( - CooAutoRegister::class.java - ).forEach { - count++ - findListenerHandlers(it) - } - val end = System.currentTimeMillis() - CooParticlesConstants.logger.info("EmittersEvents 注册完成 耗时 ${end - start} ms 扫描了 $count 个类 实际注册 :${registerHandlers.size - before}") - } - - private fun findListenerHandlers(target: SimpleClassInfo) { - val clazz = target.toClass() - if (!ParticleEvent::class.java.isAssignableFrom(clazz)) { - return - } - // 获取instance - val instance = - clazz.declaredFields.find { it.name == "INSTANCE" && Modifier.isStatic(it.modifiers) }?.get(null) - ?: clazz.getDeclaredConstructor() - .apply { isAccessible = true } - .newInstance() - register(instance as ParticleEventHandler) - } - -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt deleted file mode 100644 index a1a7f892..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.entity.Entity - -/** - * 粒子接触实体事件 - */ - -class ParticleHitEntityEvent( - override var particle: ControlableParticle, override var particleData: ControlableParticleData, var hit: Entity, -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleHitEntityEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt deleted file mode 100644 index 3175ae3c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.core.BlockPos -import net.minecraft.world.phys.BlockHitResult -import net.minecraft.world.phys.Vec3 - -/** - * 粒子接触地面事件 - */ -class ParticleOnGroundEvent( - override var particle: ControlableParticle, - override var particleData: ControlableParticleData, - /** - * 击中的方块 - */ - var hit: BlockPos, - /** - * 和方块的交点 - */ - var intersection: Vec3, - var res: BlockHitResult -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleOnGroundEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt deleted file mode 100644 index 991cc368..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.core.BlockPos - -/** - * 粒子接触实体事件 - */ -class ParticleOnLiquidEvent( - override var particle: ControlableParticle, - override var particleData: ControlableParticleData, - var hit: BlockPos -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleOnLiquidEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt deleted file mode 100644 index 0deb18c0..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/BoxEmittersShootType.kt +++ /dev/null @@ -1,78 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.type - -import cn.coostack.cooparticlesapi.barrages.HitBox -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 -import kotlin.random.Random - -class BoxEmittersShootType(val box: HitBox) : EmittersShootType { - companion object { - @JvmStatic - val CODEC: StreamCodec = - StreamCodec.of( - { buf, type -> - type as BoxEmittersShootType - val box = type.box - buf.writeDouble(box.x1) - buf.writeDouble(box.y1) - buf.writeDouble(box.z1) - buf.writeDouble(box.x2) - buf.writeDouble(box.y2) - buf.writeDouble(box.z2) - - }, { - val box = HitBox( - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - ) - BoxEmittersShootType(box) - } - ) - const val ID = "box" - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): StreamCodec { - return CODEC - } - - val random = Random(System.currentTimeMillis()) - override fun getPositions(origin: Vec3, tick: Int, count: Int): List { - return List(count) { - val x = random.nextDouble( - box.x1, - box.x2 - ) - val y = random.nextDouble( - box.y1, - box.y2 - ) - val z = random.nextDouble( - box.z1, - box.z2 - ) - origin.add(Vec3(x, y, z)) - } - } - - override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { - if (enter.length() < 1e-7) { - // 随机速度 - val p = Vec3( - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0) - ) - return p - } - return enter - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt deleted file mode 100644 index b3c660e9..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootType.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.type - -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 - -interface EmittersShootType { - /** - * 获取id 用于序列化 - */ - fun getID(): String - - /** - * 用于序列化发射类型 - */ - fun getCodec(): StreamCodec - - /** - * 获取生成粒子的位置 - * @param origin 添加offset后的位置 - * @param tick 发射器当前tick - * @param count 粒子数量 - * @return 可能返回多个粒子 - */ - fun getPositions(origin: Vec3, tick: Int, count: Int): List - - /** - * 获取粒子的初始方向 - * @param enter 输入的参数方向 - * @param pos 粒子生成位置 - * @param origin 粒子发射器的位置 - */ - fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt deleted file mode 100644 index 3553d490..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/EmittersShootTypes.kt +++ /dev/null @@ -1,74 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.type - -import cn.coostack.cooparticlesapi.barrages.HitBox -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 - -object EmittersShootTypes { - - private val types = HashMap>() - - fun register(id: String, codec: StreamCodec) { - types[id] = codec - } - - fun fromID(id: String) = types[id] - - @JvmStatic - fun point(): EmittersShootType = PointEmittersShootType() - - @JvmStatic - fun box(box: HitBox): EmittersShootType = BoxEmittersShootType(box) - - @JvmStatic - fun line(dir: Vec3, step: Double): EmittersShootType = LineEmittersShootType(dir, step) - - /** - * @param xe 粒子生成 x 相对于发射器的位置偏移量表达式 - * @param ye 粒子生成 y 相对于发射器的位置偏移量表达式 - * @param ze 粒子生成 z 相对于发射器的位置偏移量表达式 - * - * @param dxe 粒子生成时 初始速度向量x表达式 - * @param dye 粒子生成时 初始速度向量y表达式 - * @param dze 粒子生成时 初始速度向量z表达式 - */ - @JvmStatic - fun math( - xe: String, - ye: String, - ze: String, - dxe: String, - dye: String, - dze: String, - ): EmittersShootType = MathEmittersShootType() - .apply { - x = xe - y = ye - z = ze - dx = dxe - dy = dye - dz = dze - setup() - } - - init { - register( - PointEmittersShootType.ID, PointEmittersShootType.CODEC - ) - register( - BoxEmittersShootType.ID, - BoxEmittersShootType.CODEC - ) - register( - MathEmittersShootType.ID, - MathEmittersShootType.CODEC - ) - register( - LineEmittersShootType.ID, - LineEmittersShootType.CODEC - ) - } - - fun init() {} -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt deleted file mode 100644 index b70d51ed..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/LineEmittersShootType.kt +++ /dev/null @@ -1,53 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.type - -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 - - -class LineEmittersShootType(val dir: Vec3, val step: Double) : EmittersShootType { - companion object { - @JvmStatic - val CODEC: StreamCodec = - StreamCodec.of( - { buf, type -> - type as LineEmittersShootType - buf.writeVec3(type.dir) - buf.writeDouble(type.step) - - }, { - val dir = it.readVec3() - val step = it.readDouble() - LineEmittersShootType(dir, step) - } - ) - const val ID = "line" - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): StreamCodec { - return CODEC - } - - override fun getPositions( - origin: Vec3, - tick: Int, - count: Int - ): List { - return List(count) { - origin.add(dir.normalize().scale(it * step)) - } - } - - override fun getDefaultDirection( - enter: Vec3, - tick: Int, - pos: Vec3, - origin: Vec3 - ): Vec3 { - return enter - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt deleted file mode 100644 index 51d426ce..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/MathEmittersShootType.kt +++ /dev/null @@ -1,196 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.type - -import com.ezylang.evalex.Expression -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 -import java.math.BigDecimal -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.FutureTask - -/** - * 基于EvalEx 表达式的运算 - * 支持的变量 - * 对于粒子生成位置 - * t -> tick - * c -> count - * i -> 当前索引值 - * 对于粒子初始速度 - * t -> tick - * x -> posX - * y -> posY - * z -> posZ - * ox -> origin.x - * oy -> origin.y - * oz -> origin.z - */ -class MathEmittersShootType : EmittersShootType { - companion object { - @JvmStatic - val CODEC = StreamCodec.of( - { buf, type -> - type as MathEmittersShootType - buf.writeUtf(type.x) - buf.writeUtf(type.y) - buf.writeUtf(type.z) - buf.writeUtf(type.dx) - buf.writeUtf(type.dy) - buf.writeUtf(type.dz) - }, { - MathEmittersShootType().apply { - x = it.readUtf() - y = it.readUtf() - z = it.readUtf() - dx = it.readUtf() - dy = it.readUtf() - dz = it.readUtf() - setup() - } - } - ) - const val ID = "math" - } - - /** - * 位置运算表达式 - */ - var x = "0" - var y = "0" - var z = "0" - - private var xe = Expression(x) - .with("i", 0) - .and("c", 0) - .and("t", 0) - private var ye = Expression(y) - .with("i", 0) - .and("c", 0) - .and("t", 0) - private var ze = Expression(z) - .with("i", 0) - .and("c", 0) - .and("t", 0) - - /** - * 初始方向表达式 - */ - var dx = "0" - var dy = "0" - var dz = "0" - - private var dxe = Expression(dx) - .with("t", 0) - - private var dye = Expression(dy) - .with("t", 0) - - private var dze = Expression(dz) - .with("t", 0) - - fun setup() { - xe = Expression(x) - .with("i", 0) - .and("c", 0) - .and("t", 0) - ye = Expression(y) - .with("i", 0) - .and("c", 0) - .and("t", 0) - ze = Expression(z) - .with("i", 0) - .and("c", 0) - .and("t", 0) - dxe = Expression(dx) - .with("t", 0) - - dye = Expression(dy) - .with("t", 0) - - dze = Expression(dz) - .with("t", 0) - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): StreamCodec { - return CODEC - } - - override fun getPositions( - origin: Vec3, - tick: Int, - count: Int - ): List { - - return List(count) { - val taskX = CompletableFuture.supplyAsync { - xe.with("t", tick) - .with("c", count) - .with("i", it) - .evaluate().numberValue.toDouble() - } - - val taskY = CompletableFuture.supplyAsync { - ye.with("t", tick) - .with("c", count) - .with("i", it) - .evaluate().numberValue.toDouble() - } - - val taskZ = CompletableFuture.supplyAsync { - ze.with("t", tick) - .and("c", count) - .and("i", it) - .evaluate().numberValue.toDouble() - } - CompletableFuture.allOf(taskX, taskY, taskZ).join() - origin.add(Vec3(taskX.get(), taskY.get(), taskZ.get())) - } - } - - override fun getDefaultDirection( - enter: Vec3, - tick: Int, - pos: Vec3, - origin: Vec3 - ): Vec3 { - val x = CompletableFuture.supplyAsync { - dxe.with("t", tick) - .and("x", pos.x) - .and("y", pos.y) - .and("z", pos.z) - .and("ox", origin.x) - .and("oy", origin.y) - .and("oz", origin.z) - .evaluate().numberValue.toDouble() - } - val y = CompletableFuture.supplyAsync { - dye.with("t", tick) - .and("x", pos.x) - .and("y", pos.y) - .and("z", pos.z) - .and("ox", origin.x) - .and("oy", origin.y) - .and("oz", origin.z) - .evaluate().numberValue.toDouble() - } - val z = CompletableFuture.supplyAsync { - dze.with("t", tick) - .and("x", pos.x) - .and("y", pos.y) - .and("z", pos.z) - .and("ox", origin.x) - .and("oy", origin.y) - .and("oz", origin.z) - .evaluate().numberValue.toDouble() - } - CompletableFuture.allOf(x, y, z).join() - return enter.add( - x.get(), y.get(), z.get() - ) - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt deleted file mode 100644 index 72df0b76..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/type/PointEmittersShootType.kt +++ /dev/null @@ -1,48 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.type - -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.world.phys.Vec3 -import kotlin.random.Random - -class PointEmittersShootType : EmittersShootType { - val random = Random(System.currentTimeMillis()) - - companion object { - @JvmStatic - val CODEC: StreamCodec = - StreamCodec.of( - { b, p -> }, { - PointEmittersShootType() - } - ) - const val ID = "point" - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): StreamCodec { - return CODEC - } - - override fun getPositions(origin: Vec3, tick: Int, count: Int): List { - return List(count) { - origin - } - } - - override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { - if (enter.length() < 1e-7) { - // 随机速度 - val p = Vec3( - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0) - ) - return p - } - return enter - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt deleted file mode 100644 index f98bbc30..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt +++ /dev/null @@ -1,573 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C -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.cparticle.CParticleSystem -import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable -import cn.coostack.cooparticlesapi.cparticle.compat.CParticleDisplayer -import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.particles.control.ControlParticleManager -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.particles.control.ParticleControler -import cn.coostack.cooparticlesapi.particles.control.RemoveReason -import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup -import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import cn.coostack.cooparticlesapi.utils.Math3DUtil -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap -import kotlin.collections.set -import kotlin.math.PI - -/** 客户端渲染和服务端处理都用这个类 */ -@Deprecated("使用ParticleComposition") -abstract class ParticleGroupStyle(var visibleRange: Double = 32.0, val uuid: UUID = UUID.randomUUID()) : - Controlable, ServerControler, Tickable { - var world: Level? = null - var pos: Vec3 = Vec3.ZERO - var client = false - var rotate = 0.0 - var axis = RelativeLocation.yAxis() - var scale = 1.0 - set(value) { - field = value.coerceAtLeast(0.001) - } - - var positionDirty = false - - - /** - * 上一次更新的游戏时间 - * > 用来解决回放的时候会产生额外的粒子 或者导致缺少粒子 (额外tick) - */ - - var lastUpdatedGameTime = 0L - - /** 生成粒子样式的时间 可能会直接跳转到其他时候 然后这个时候这个参数就需要进行同步 */ - var displayedTime = 0L - - /** 自动发包同步到客户端 可能会占据大量带宽 */ - var autoToggle = false - - var displayed = false - internal set - internal var valid = true - internal val invokeQueue = ArrayList Unit>() - internal val postInvokeQueue = ArrayList Unit>() - val particles = ConcurrentHashMap>() - val particleLocations = ConcurrentHashMap, RelativeLocation>() - private val cParticleSystems = LinkedHashMap() - private val cParticleContainers = LinkedHashSet>() - - internal fun getCParticleSystems(): List { - removeReleasedCParticleSystems() - return cParticleSystems.keys.toList() - } - - internal fun getCParticleContainers(): List> = - cParticleContainers.toList() - - protected fun prepareCParticleDisplayer(displayer: ParticleDisplayer, capacity: Int) { - if (displayer is CParticleDisplayer) { - displayer.bindDedicatedSystemIfAbsent("style/$uuid", capacity, pos) - } - } - - protected fun registerCParticleNode(controler: Controlable<*>) { - removeReleasedCParticleSystems() - when (controler) { - is CParticleControlable -> cParticleSystems.merge(controler.system, 1, Int::plus) - is ParticleComposition, - is ParticleGroupStyle, - is ControlableParticleGroup -> cParticleContainers.add(controler) - } - } - - protected fun unregisterCParticleNode(controler: Controlable<*>) { - when (controler) { - is CParticleControlable -> { - val count = cParticleSystems[controler.system] ?: return - if (count <= 1) cParticleSystems.remove(controler.system) - else cParticleSystems[controler.system] = count - 1 - } - - is ParticleComposition, - is ParticleGroupStyle, - is ControlableParticleGroup -> cParticleContainers.remove(controler) - } - } - - private fun removeReleasedCParticleSystems() { - cParticleSystems.keys.removeIf { it.released } - } - - override fun isValid(): Boolean { - return valid - } - - /** 当粒子组合初始化时, 存储1倍缩放粒子组与原点的距离 */ - val particleDefaultLength = ConcurrentHashMap() - - - abstract fun getCurrentFrames(): Map - - abstract fun onDisplay() - - /** - * 服务器同步到客户端时, 执行的代码 基类已经存储的参数 如 pos, world ,rotate, axis, scale, uuid 无需同步 - * 自定义的其他参数需要同步 - * - * 如果 autoToggle = true 则会在每个tick都会执行发包 反复调用此方法 - */ - abstract fun writePacketArgs(): Map> - - /** - * 客户端接受服务器的同步时, 使用的代码 - * - * 同步内容如下 -> 客户端创建此类时 输入的基本参数 包括了你在 writePacketArgs输入的参数 客户端在类内存在其余更改时 - * 服务器传入的参数 比如自己设定的一些其他参数值 - * - * 无需处理以下参数 pos world rotate axis scale uuid 其余参数自行处理 - */ - abstract fun readPacketArgs(args: Map>) - - open fun beforeDisplay(styles: Map) { - } - - override fun addPreTickAction(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { - invokeQueue.add(action) - return this - } - - override fun addPreTickActionPost(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { - postInvokeQueue.add(action) - return this - } - - override fun controlUUID(): UUID { - return uuid - } - - override fun rotateToPoint(to: RelativeLocation) { - Math3DUtil.rotatePointsToPoint( - particleLocations.values.toList(), to, axis - ) - axis = to - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to) - ) - ) - } - } - - override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { -// Math3DUtil.rotateAsAxis( -// particleLocations.values.toList(), axis, angle -// ) -// Math3DUtil.rotatePointsToPoint( -// particleLocations.values.toList(), to, axis -// ) - Math3DUtil.rotateToWithRoll( - particleLocations.values.toList(), axis, to, radian - ) - axis = to - this.rotate += radian - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to), - "rotate_angle" to ParticleControlerDataBuffers.double(radian) - ) - ) - } - } - - override fun rotateAsAxis(angle: Double) { - Math3DUtil.rotateAsAxis( - particleLocations.values.toList(), axis, angle - ) - this.rotate += angle - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "rotate_angle" to ParticleControlerDataBuffers.double(angle) - ) - ) - } - } - - override fun teleportTo(pos: Vec3) { - this.pos = pos - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "teleport" to ParticleControlerDataBuffers.vec3d(pos) - ) - ) - } - } - - override fun teleportTo(x: Double, y: Double, z: Double) { - teleportTo(Vec3(x, y, z)) - } - - override fun remove() { - clear(false) - if (!client) { - // 同步到其他客户端 - ParticleStyleManager.filterVisiblePlayer(this).forEach { - val player = world!!.getPlayerByUUID(it) as? ServerPlayer - player ?: return@forEach - CooParticlesServices.SERVER_NETWORK.send( - PacketParticleStyleS2C( - uuid, ControlType.REMOVE, mapOf() - ), player - ) - } - } - } - - /** Controler需求 */ - override fun getControlObject(): ParticleGroupStyle { - return this - } - - /** ServerControler需求 */ - override fun getValue(): ParticleGroupStyle { - return this - } - - /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ - fun change(toggleMethod: ParticleGroupStyle.() -> Unit, args: Map>) { - if (client) { - return - } - toggleMethod(this) - // 发包 - ParticleStyleManager.filterVisiblePlayer(this).forEach { - val player = world!!.getPlayerByUUID(it) ?: return@forEach - CooParticlesServices.SERVER_NETWORK.send( - PacketParticleStyleS2C( - uuid, ControlType.CHANGE, args - ), player as ServerPlayer - ) - } - } - - /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ - fun change(args: Map>) { - change({}, args) - } - - open fun display(pos: Vec3, world: Level) { - if (displayed) { - return - } - displayed = true - this.displayedTime = world.gameTime - this.lastUpdatedGameTime = world.gameTime - this.pos = pos - this.world = world - this.client = world.isClientSide - if (!client) { - // 服务器只负责数据同步 不负责粒子生成 - onDisplay() - return - } - flush() - onDisplay() - } - - open fun flush() { - if (particles.isNotEmpty()) { - clear(true) - } - displayParticles() - } - - open fun toggleRelative() { - if (!client) { - return - } - val iterator = particleLocations.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - val particle = entry.key - val rl = entry.value - particle.teleportTo( - pos.add( - rl.toVector() - ) - ) - } - positionDirty = false - } - - - private fun spawnSingleParticle(data: StyleData, rel: RelativeLocation) { - val displayer = data.displayerBuilder(data.uuid) - if (displayer !is ParticleDisplayer.SingleParticleDisplayer) return - val controler = ControlParticleManager.createControl(data.uuid) - controler.applyInitializedAction(data.particleHandler) - val toPos = Vec3(pos.x + rel.x, pos.y + rel.y, pos.z + rel.z) - displayer.display(toPos, world as ClientLevel) ?: return - data.particleControlerHandler(controler) - // 把粒子丢回生成列表 - particles[data.uuid] = controler - particleLocations[controler] = rel - } - - override fun tick() { - if (!displayed || !valid) { - clear(false) - return - } - val current = world!!.gameTime - if (!displayed || !valid) { - clear(false) - return - } - invokeQueue.forEach { - it(this) - } - - val iterator = particles.iterator() - while (iterator.hasNext()) { - val style = iterator.next() - when (val value = style.value) { - is Tickable<*> -> { - value.tick() - } - } - } - postInvokeQueue.forEach { - it(this) - } - this.lastUpdatedGameTime = current - } - - fun toggleScale(locations: Map) { - if (!valid) { - // remove过后 无法同步 - return - } - if (particleDefaultLength.isEmpty()) { - locations.forEach { - val uuid = it.key.uuid - particleDefaultLength[uuid] = it.value.length() - } - } - locations.forEach { - val uuid = it.key.uuid - val len = particleDefaultLength[uuid] ?: return@forEach - if (len <= 0.0) { - return@forEach - } - val value = it.value - value.multiply(len * scale / value.length()) - } - } - - open fun scale(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale can not be less than zero") - return - } - scale = new - // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 - // 多为服务端(除非有人使在Client环境创建了这个类) - if (displayed) { - toggleScaleDisplayed() - } - // 发包有效 - if (!valid) { - // remove过后 无法同步 - return - } - if (!client) { - change(mapOf("scale" to ParticleControlerDataBuffers.double(new))) - } - } - - protected open fun toggleScaleDisplayed() { - particleLocations.forEach { - val uuid = it.key.controlUUID() - val len = particleDefaultLength[uuid]!! - val value = it.value - if (len in -1e-3..1e-3) return@forEach - value.multiply(len * scale / value.length()) - } - toggleRelative() - } - - override fun spawn(world: Level, pos: Vec3) { - if (world !is ServerLevel) return - ParticleStyleManager.spawnStyle(world, pos, this) - } - - open fun preRotateTo(map: Map, to: RelativeLocation) { - Math3DUtil.rotatePointsToPoint( - map.values.toList(), to, axis - ) - this.axis = to - } - - open fun preRotateAsAxis(map: Map, axis: RelativeLocation, angle: Double) { - Math3DUtil.rotateAsAxis( - map.values.toList(), axis, angle - ) - this.axis = axis - } - - open fun preRotateAsAxis(map: Map, angle: Double) { - Math3DUtil.rotateAsAxis( - map.values.toList(), axis, angle - ) - } - - private fun displayParticles() { - val locations = getCurrentFrames() - beforeDisplay(locations) - toggleScale(locations) - Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) - locations.forEach { - val data = it.key - val uuid = it.key.uuid - val rl = it.value - val displayer = it.key.displayerBuilder(uuid) - prepareCParticleDisplayer(displayer, locations.size) - if (displayer is ParticleDisplayer.SingleParticleDisplayer) { - val controler = ControlParticleManager.createControl(uuid) - controler.applyInitializedAction(data.particleHandler) - } - val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) - val controler = displayer.display(toPos, world as ClientLevel) ?: return@forEach - if (controler is ParticleControler) { - data.particleControlerHandler(controler) - } - registerCParticleNode(controler) - - particles[uuid] = controler - particleLocations[controler] = rl - } - } - - override fun remove(reason: RemoveReason) { - remove() - } - - open internal fun clear(valid: Boolean) { - particles.forEach { - it.value.remove() - } - particles.clear() - particleLocations.clear() - particleDefaultLength.clear() - cParticleSystems.clear() - cParticleContainers.clear() - this.valid = valid - } - - open class StyleData( - val displayerBuilder: (UUID) -> ParticleDisplayer, - ) { - val uuid: UUID = UUID.randomUUID() - var particleHandler: ControlableParticle.() -> Unit = {} - var particleControlerHandler: ParticleControler.() -> Unit = {} - fun withParticleHandler( - builder: ControlableParticle.() -> Unit - ): StyleData { - particleHandler = builder - return this - } - - fun withParticleControlerHandler( - builder: ParticleControler.() -> Unit - ): StyleData { - particleControlerHandler = builder - return this - } - } - - /** 为了提高傻逼StyleData的复用性 专门设置此类 */ - open class StyleDataBuilder() { - private var displayerBuilder: (UUID) -> ParticleDisplayer = - { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } - private val particleHandlers = mutableListOf Unit>() - private val particleControlerHandlers = mutableListOf Unit>() - fun addParticleHandler( - builder: ControlableParticle.() -> Unit - ): StyleDataBuilder { - particleHandlers.add(builder) - return this - } - - fun addParticleControlerHandler( - builder: ParticleControler.() -> Unit - ): StyleDataBuilder { - particleControlerHandlers.add(builder) - return this - } - - fun clearParticleHandlers(): StyleDataBuilder { - particleHandlers.clear() - return this - } - - fun clearParticleControlers(): StyleDataBuilder { - particleControlerHandlers.clear() - return this - } - - fun removeHandler(index: Int): StyleDataBuilder { - if (index in particleHandlers.indices) particleHandlers.removeAt(index) - return this - } - - fun removeParticleControler(index: Int): StyleDataBuilder { - if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) - return this - } - - fun displayer(builder: (UUID) -> ParticleDisplayer): StyleDataBuilder { - this.displayerBuilder = builder - return this - } - - fun build(): StyleData = StyleData(displayerBuilder) - .withParticleHandler { - particleHandlers.forEach { it() } - }.withParticleControlerHandler { - particleControlerHandlers.forEach { it() } - } - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt deleted file mode 100644 index 25e9c85d..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt +++ /dev/null @@ -1,222 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper -import net.minecraft.client.particle.ParticleRenderType -import net.minecraft.world.entity.player.Player -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -/** - * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) - * 专门创建此类 - * - * 此类没有Provider 因此无法单独生成 - * - * 继承此类的目的是为这个类创建一个专属形状的子类 - * 这种子类可以拥有Provider - */ -open class ParticleShapeStyle(uuid: UUID) : - ParticleGroupStyle(64.0, uuid) { - var scaleHelper: ScaleHelper? = null - - /** - * 增加可复用性 - */ - private val displayInvokes = mutableListOf Unit>() - private val beforeDisplayInvokes = mutableListOf) -> Unit>() - private val pointBuilders = LinkedHashMap StyleData>() - - /** - * 通过判断(外层) 提供的状态工具来决定是否反转缩放 - * 在toggleDisplay执行这个方法 - */ - val reverseFunctionFromStatus: (ParticleShapeStyle, StyleStatusHelper) -> Unit = function@{ it, displayStatus -> - // 必须设置缩放工具 - if (scaleHelper == null) return@function - it.addPreTickAction { - if (it.scaleReversed && it.scaleHelper!!.isZero()) { - it.remove() - return@addPreTickAction - } - if (displayStatus.displayStatus != 2) { - return@addPreTickAction - } - if (!it.scaleReversed) { - it.scaleReversed(false) - } - } - } - var spawnAge = 0 - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - private set - - fun appendBuilder(pointsBuilder: PointsBuilder, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { - pointBuilders[pointsBuilder] = dataBuilder - return this - } - - fun appendPoint(point: RelativeLocation, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { - pointBuilders[ - PointsBuilder().also { it.addPoint(point) } - ] = dataBuilder - return this - } - - /** - * @param max 直接让scale从最大开始 - */ - fun scaleReversed(max: Boolean): ParticleShapeStyle { - scaleHelper ?: return this - scaleReversed = true - if (max) { - scaleHelper!!.toggleScale(scaleHelper!!.maxScale) - } - return this - } - - fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): ParticleShapeStyle { - scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - fun loadScaleHelperBezierValue( - minScale: Double, - maxScale: Double, - scaleTick: Int, - c1: RelativeLocation, - c2: RelativeLocation - ): ParticleShapeStyle { - scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - /** - * 在display之前执行 - * 可以多次执行此方法, 不会发生方法覆盖 - */ - fun toggleOnDisplay(toggleMethod: ParticleShapeStyle.() -> Unit): ParticleShapeStyle { - displayInvokes.add(toggleMethod) - return this - } - - /** - * 在生成粒子之前执行 - */ - fun toggleBeforeDisplay(toggleMethod: ParticleShapeStyle.(Map) -> Unit): ParticleShapeStyle { - beforeDisplayInvokes.add(toggleMethod) - return this - } - - override fun beforeDisplay(styles: Map) { - beforeDisplayInvokes.forEach { it(styles) } - } - - override fun getCurrentFrames(): Map { - val res = HashMap() - pointBuilders.forEach { entry -> - res.putAll(entry.key.createWithStyleData { entry.value(it) }) - } - return res - } - - override fun isValid(): Boolean { - return valid - } - - override fun onDisplay() { - displayInvokes.forEach { - it() - } - addPreTickAction { - spawnAge++ - if (!scalePreTick || scaleHelper == null) { - return@addPreTickAction - } - - if (scaleReversed) { - scaleHelper!!.doScaleReversed() - } else { - scaleHelper!!.doScale() - } - } - } - - override fun writePacketArgs(): Map> { - return mapOf() - } - - override fun readPacketArgs(args: Map>) { - } - - fun fastRotateToPlayerView(player: Player) { - rotateToPoint(RelativeLocation.of(player.forward)) - } - - fun fastStyleData(color: Vec3, displayer: (UUID) -> ParticleDisplayer): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - } - } - - fun fastStyleData(color: Vec3, sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - } - } - - fun fastStyleData( - color: Vec3, - sheet: ParticleRenderType, - size: Float, - displayer: (UUID) -> ParticleDisplayer - ): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - } - } - - fun fastStyleData( - color: Vec3, - sheet: ParticleRenderType, - size: Float, - alpha: Float, - displayer: (UUID) -> ParticleDisplayer - ): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - this.particleAlpha = alpha - } - } - - fun fastStyleData(sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { - return StyleData(displayer).withParticleHandler { - this.textureSheet = sheet - } - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt deleted file mode 100644 index dd68ba0c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt +++ /dev/null @@ -1,259 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.CooParticlesAPI -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.client.Minecraft -import net.minecraft.server.level.ServerPlayer -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 - -/** - * TODO 重构Style使得让自动更新默认打开(如果更新时发现不存在那就重新生成) - * - * @constructor Create empty Particle style manager - */ -@Deprecated("使用ParticleComposition") -object ParticleStyleManager { - - /** - * 服务端拥有的 -> (Server View)客户层是empty - */ - val serverViewStyles = ConcurrentHashMap() - - /** - * 玩家可以看见的 style - */ - internal val visible = ConcurrentHashMap>() - - /** - * 当前客户端可见的 (Client View)服务层是empty - */ - val clientViewStyles = ConcurrentHashMap() - - private val registerBuilders = - HashMap, ParticleStyleProvider>() - - /** - * 在ClientModInitializer 注册ParticleStyle 用于服务器同步 - */ - // Keep this as an instance method for binary compatibility with existing consumers. - fun register( - type: Class, - provider: ParticleStyleProvider - ) { - registerBuilders[type] = provider - } - - fun getBuilder(type: Class): ParticleStyleProvider? { - return registerBuilders[type] - } - - @JvmStatic - fun spawnStyle(world: Level, pos: Vec3, style: ParticleGroupStyle) { - if (world.isClientSide) { - // 生成粒子 - style.display(pos, world) - clientViewStyles[style.uuid] = style - return - } - style.display(pos, world) - serverViewStyles[style.uuid] = style - // 发送数据包 -> 包括type - world.players().filter { style.pos.distanceTo(it.position()) <= style.visibleRange }.forEach { - addStylePlayerView(it as ServerPlayer, style) - } - } - - - fun doTickClient() { - val player = Minecraft.getInstance().player ?: return - if (player.isDeadOrDying) { - clientViewStyles.clear() - return - } - val iterator = clientViewStyles.iterator() - while (iterator.hasNext()) { - val style = iterator.next().value - style.tick() - if (!style.valid) { - iterator.remove() - } - } - } - - fun doTickServer() { - val iterator = serverViewStyles.iterator() - while (iterator.hasNext()) { - val style = iterator.next().value - // 更新可见性 - upgradeVisible(style) - style.tick() - if (style.autoToggle) { - style.world!!.players().forEach { - val visibleSet = visible.getOrPut(it.uuid) { HashSet() } - // 不可见的粒子没有必要同步 - if (!visibleSet.contains(style)) { - return@forEach - } - CooParticlesServices.SERVER_NETWORK.send( - buildAutoTogglePacket(style), - it as ServerPlayer, - ) - } - } - if (!style.valid) { - filterVisiblePlayer(style).forEach { - val player = style.world!!.getPlayerByUUID(it) ?: return@forEach - removeGroupPlayerView(player as ServerPlayer, style) - visible[it]?.remove(style) - } - iterator.remove() - } - } - val playerVisibleIterator = visible.iterator() - while (playerVisibleIterator.hasNext()) { - val playerUUID = playerVisibleIterator.next().key - // 判断玩家是否在线 - val server = CooParticlesAPI.serverOrNull ?: return - val player = server.playerList.getPlayer(playerUUID) - if (player == null) { - playerVisibleIterator.remove() - } - } - } - - fun filterVisiblePlayer(group: ParticleGroupStyle): Set { - val set = HashSet() - visible.forEach { - if (group in it.value) { - set.add(it.key) - } - } - return set - } - - private fun upgradeVisible(style: ParticleGroupStyle) { - val server = CooParticlesAPI.serverOrNull ?: return - server.playerList.players.forEach { p -> - val visibleSet = visible.getOrPut(p.uuid) { HashSet() } - if (p.level().dimension() != style.world?.dimension()) { - // 世界转换 - if (style in visibleSet) { - removeGroupPlayerView(p, style) - visibleSet!!.remove(style) - } - return@forEach - } - if (p.isDeadOrDying) { - if (style in visibleSet) { - removeGroupPlayerView(p, style) - visibleSet!!.remove(style) - } - return@forEach - } - if (style.pos.distanceTo(p.position()) <= style.visibleRange) { - // 防止重复添加(发包) - if (style in visibleSet) { - return@forEach - } - addStylePlayerView(p, style) - } else { - if (style in visibleSet) { - removeGroupPlayerView(p, style) - } - // 超过范围 - visibleSet.remove(style) - } - } - } - - /** - * 在该玩家的视角中移除这些粒子 - * @param target - * @param targetGroup - */ - private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ParticleGroupStyle) { - // 发包给玩家 - val packet = PacketParticleStyleS2C( - targetGroup.uuid, - ControlType.REMOVE, mapOf() - ) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - /* - * @param target - * @param targetStyle - */ - private fun addStylePlayerView( - target: ServerPlayer, - targetStyle: ParticleGroupStyle, - ) { - // 修复一个包发送给一个玩家两次 - val visibleSet = visible.getOrPut(target.uuid) { HashSet() } - visibleSet.add(targetStyle) - // 发包给玩家 - val packet = buildCreatePacket(targetStyle, targetStyle.pos) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - private fun buildAutoTogglePacket( - style: ParticleGroupStyle, - ): PacketParticleStyleS2C { - - /** - * SequencedParticleStyle - * 你赢了 - * addMultiple的index 能从 7 自动同步成 0 - * - * 一看 server side 诶 index没有问题 - * 一看args 诶 index没有问题 - * 但是呢 - * 传入客户端之后啊 - * 7 变成 0 了捏 - * 不知道的以为是Packet的 CODEC被你妈吃数据包了 - */ - - return PacketParticleStyleS2C( - style.uuid, - ControlType.CHANGE, - mapOf( - *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() - ) - ) - } - - private fun buildCreatePacket(style: ParticleGroupStyle, pos: Vec3): PacketParticleStyleS2C = - PacketParticleStyleS2C( - style.uuid, - ControlType.CREATE, - mapOf( - "style_type" to ParticleControlerDataBuffers.string(style::class.java.name), - "pos" to ParticleControlerDataBuffers.vec3d(pos), - "rotate" to ParticleControlerDataBuffers.double(style.rotate), - "axis" to ParticleControlerDataBuffers.vec3d(style.axis.toVector()), - "scale" to ParticleControlerDataBuffers.double(style.scale), - "lastUpdatedGameTime" to ParticleControlerDataBuffers.long(style.lastUpdatedGameTime), - "displayedTime" to ParticleControlerDataBuffers.long(style.displayedTime), - *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() - ) - ) - - fun clearAllVisible() { - clientViewStyles.onEach { - it.value.remove() - }.clear() - } - - fun clearServer() { - serverViewStyles.clear() - visible.clear() - } -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt deleted file mode 100644 index acca17be..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import java.util.UUID - -@Deprecated("使用ParticleComposition") -interface ParticleStyleProvider { - /** - * 创建style - * 执行此方法的时候不要调用基本参数(因为还没初始化) - */ - fun createStyle(uuid: UUID, args: Map>): ParticleGroupStyle -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt deleted file mode 100644 index d8a36a12..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt +++ /dev/null @@ -1,288 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper -import net.minecraft.client.particle.ParticleRenderType -import net.minecraft.world.entity.player.Player -import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.TreeMap -import java.util.UUID -import java.util.function.Predicate - -/** - * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) - * 专门创建此类 - * - * 无法单独生成 - * 继承此类的目的是为这个类创建一个专属形状的子类 - * client属性始终为true - */ -class SequencedParticleShapeStyle(uuid: UUID) : - SequencedParticleStyle(64.0, uuid) { - var scaleHelper: ScaleHelper? = null - - private val displayInvokes = mutableListOf Unit>() - private val beforeDisplayInvokes = - mutableListOf) -> Unit>() - - private val pointBuilders = LinkedHashMap SortedStyleData>() - - /** - * 用于控制粒子的播放顺序 - * 如果满足 animationConditions[animationIndex].first的条件 - * 则会执行 addMultiple(second (second > 0)) - * 或者 removeMultiple(second (second < 0) - * 符合条件后 animationIndex 会递增 - * 直到到达边界 - */ - private val animationConditions = ArrayList, Int>>() - var animationIndex = 0 - private set - - /** - * 通过判断(外层) 提供的状态工具来决定是否反转缩放 - * 在toggleDisplay执行这个方法 - */ - val reverseFunctionFromStatus: (SequencedParticleShapeStyle, StyleStatusHelper) -> Unit = - function@{ it, displayStatus -> - // 必须设置缩放工具 - if (scaleHelper == null) return@function - it.addPreTickAction { - if (it.scaleReversed && it.scaleHelper!!.isZero()) { - it.remove() - return@addPreTickAction - } - if (displayStatus.displayStatus != 2) { - return@addPreTickAction - } - if (!it.scaleReversed) { - it.scaleReversed(false) - } - } - } - var spawnAge = 0 - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - private set - - fun appendBuilder( - pointsBuilder: PointsBuilder, - dataBuilder: (RelativeLocation) -> SortedStyleData - ): SequencedParticleShapeStyle { - pointBuilders[pointsBuilder] = dataBuilder - return this - } - - fun appendPoint( - point: RelativeLocation, - dataBuilder: (RelativeLocation) -> SortedStyleData - ): SequencedParticleShapeStyle { - pointBuilders[ - PointsBuilder().also { it.addPoint(point) } - ] = dataBuilder - return this - } - - /** - * @param predicate 设置添加/删除动画的要求 - * @param add 向下的个数 - */ - fun appendAnimateCondition( - predicate: Predicate, - add: Int - ): SequencedParticleShapeStyle { - animationConditions.add(predicate to add) - return this - } - - /** - * @param max 直接让scale从最大开始 - */ - fun scaleReversed(max: Boolean): SequencedParticleShapeStyle { - scaleHelper ?: return this - scaleReversed = true - if (max) { - scaleHelper!!.toggleScale(scaleHelper!!.maxScale) - } - return this - } - - fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): SequencedParticleShapeStyle { - scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - fun loadScaleHelperBezierValue( - minScale: Double, - maxScale: Double, - scaleTick: Int, - c1: RelativeLocation, - c2: RelativeLocation - ): SequencedParticleShapeStyle { - scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - /** - * 在display之前执行 - */ - fun toggleOnDisplay(toggleMethod: SequencedParticleShapeStyle.() -> Unit): SequencedParticleShapeStyle { - displayInvokes.add(toggleMethod) - return this - } - - /** - * 在生成粒子之前执行 - */ - fun toggleBeforeDisplay(toggleMethod: SequencedParticleShapeStyle.(SortedMap) -> Unit): SequencedParticleShapeStyle { - beforeDisplayInvokes.add(toggleMethod) - return this - } - - - override fun beforeDisplay(styles: SortedMap) { - beforeDisplayInvokes.forEach { - it(styles) - } - } - - - override fun onDisplay() { - displayInvokes.forEach { it() } - addPreTickAction { - if (animationIndex >= animationConditions.size) { - return@addPreTickAction - } - val (predicate, add) = animationConditions[animationIndex] - if (predicate.test(this@SequencedParticleShapeStyle)) { - if (add > 0) { - addMultiple(add) - } else { - removeMultiple(add) - } - animationIndex++ - } - } - addPreTickAction { - spawnAge++ - if (!scalePreTick || scaleHelper == null) { - return@addPreTickAction - } - - if (scaleReversed) { - scaleHelper!!.doScaleReversed() - } else { - scaleHelper!!.doScale() - } - } - } - - private var count = -1 - override fun getParticlesCount(): Int { - // 调用此方法比初始化count早 - if (count == -1 || count == 0) { - count = getCurrentFramesSequenced().size - } - return count - } - - override fun getCurrentFramesSequenced(): SortedMap { - val res = TreeMap() - pointBuilders.forEach { entry -> - res.putAll(entry.key.createWithSequencedStyleData { it, order -> - entry.value(it) - }) - } - return res - } - - override fun writePacketArgsSequenced(): Map> { - return mapOf() - } - - override fun readPacketArgsSequenced(args: Map>) { - } - - - fun fastRotateToPlayerView(player: Player) { - rotateToPoint(RelativeLocation.of(player.forward)) - } - - fun fastStyleData(order: Int, color: Vec3, displayer: (UUID) -> ParticleDisplayer): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - color: Vec3, - sheet: ParticleRenderType, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - color: Vec3, - sheet: ParticleRenderType, - size: Float, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - color: Vec3, - sheet: ParticleRenderType, - size: Float, - alpha: Float, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - this.particleAlpha = alpha - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - sheet: ParticleRenderType, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.textureSheet = sheet - } as SortedStyleData - } -} \ No newline at end of file diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt deleted file mode 100644 index f9825d8c..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt +++ /dev/null @@ -1,551 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.particles.ControlableParticle -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.impl.ControlableEndRodEffect -import cn.coostack.cooparticlesapi.utils.Math3DUtil -import cn.coostack.cooparticlesapi.utils.MathDataUtil -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.UUID -import kotlin.collections.toList -import kotlin.math.PI - - -/** - * 几个免除手动书写 SequencedParticleStyle的方法 - * - * 在这里不建议使用 autoToggle方法 会导致一些奇奇怪怪的问题 (原因未知) 可能是 部分index同步错误导致 - */ -@Deprecated("使用ParticleComposition") -abstract class SequencedParticleStyle(visibleRange: Double = 32.0, uuid: UUID = UUID.randomUUID()) : - ParticleGroupStyle(visibleRange, uuid) { - companion object { - const val CREATE_PARTICLE = 1 - const val DELETE_PARTICLE = 0 - } - - - class SortedStyleData(displayerBuilder: (UUID) -> ParticleDisplayer, val order: Int) : - StyleData(displayerBuilder), Comparable { - override fun compareTo(other: SortedStyleData): Int { - return order - other.order - } - } - - - class SortedStyleDataBuilder { - private var displayerBuilder: (UUID) -> ParticleDisplayer = - { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } - private val particleHandlers = mutableListOf Unit>() - private val particleControlerHandlers = mutableListOf Unit>() - fun addParticleHandler( - builder: ControlableParticle.() -> Unit - ): SortedStyleDataBuilder { - particleHandlers.add(builder) - return this - } - - fun addParticleControlerHandler( - builder: ParticleControler.() -> Unit - ): SortedStyleDataBuilder { - particleControlerHandlers.add(builder) - return this - } - - fun clearParticleHandlers(): SortedStyleDataBuilder { - particleHandlers.clear() - return this - } - - fun clearParticleControlers(): SortedStyleDataBuilder { - particleControlerHandlers.clear() - return this - } - - fun removeHandler(index: Int): SortedStyleDataBuilder { - if (index in particleHandlers.indices) particleHandlers.removeAt(index) - return this - } - - fun removeParticleControler(index: Int): SortedStyleDataBuilder { - if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) - return this - } - - fun displayer(builder: (UUID) -> ParticleDisplayer): SortedStyleDataBuilder { - this.displayerBuilder = builder - return this - } - - fun build(order: Int): SortedStyleData = SortedStyleData(displayerBuilder, order) - .withParticleHandler { - particleHandlers.forEach { it() } - }.withParticleControlerHandler { - particleControlerHandlers.forEach { it() } - } as SortedStyleData - } - - /** - * 在服务器处是0 size的大小 client用于存储某个粒子/粒子组 的展示情况 - * - * 用于彻底解除强制重写的烦恼 - * - * FIXED 修复多余的内存占用 - */ - val displayedStatus: LongArray by lazy { - val count = getParticlesCount() - var page = count / 64 - if (count % 64 > 0) { - page++ - } - LongArray(page) - } - - - /** 统计对应的粒子顺序 (uuid) */ - protected var sequencedParticles = ArrayList>() - private set - - - var displayedParticleCount = 0 - private set(value) { - field = value.coerceAtLeast(0) - } - - /** - * 此参数所指向的状态为false - * - * 服务器可用 不会同步到客户端 - */ - var particleLinkageDisplayCurrentIndex = 0 - private set - - /** - * @return 当前粒子样式的样式个数 (客户端执行getCurrentFramesSequenced返回的集合长度) 请手动计算粒子个数 - * 或者维护一个变量 支持服务器和客户端同时生成 请勿直接在服务器中调用 getCurrentFramesSequenced().size - * 否则会导致服务器崩溃 - */ - abstract fun getParticlesCount(): Int - abstract fun getCurrentFramesSequenced(): SortedMap - abstract fun writePacketArgsSequenced(): Map> - abstract fun readPacketArgsSequenced(args: Map>) - - /** 服务器发包 -> index_status_change -> intArray index, status */ - fun addSingle() { - if (client) { - // 适配在纯客户端页面的添加 - // 由于和服务器无关所以大多数参数无需设置 - if (particleLinkageDisplayCurrentIndex >= getParticlesCount()) { - return - } - toggleFromStatus(particleLinkageDisplayCurrentIndex++, true) - return - } - displayedParticleCount++ - if (getStatus(particleLinkageDisplayCurrentIndex)) { - return - } - setStatus(particleLinkageDisplayCurrentIndex, true) - // 处理服务器 - // 发包 - val args = buildChangeSingleStatusArgs( - particleLinkageDisplayCurrentIndex++, - true - ) - change({}, mapOf(args)) - } - - /** - * 服务器发包 -> indexes_status_change_arg -> intArray indexes - * indexes_status_change_method -> intArray status method - */ - fun addMultiple(count: Int) { - if (client) { - repeat(count) { - addSingle() - } - return - } - if (count <= 0) { - return - } - if (count == 1) { - addSingle() - return - } - displayedParticleCount += count - // 处理服务器 - // 发包 - val indexes = particleLinkageDisplayCurrentIndex..> { - return mapOf( - "indexes_status_change_arg" to ParticleControlerDataBuffers.intArray(indexes), - "indexes_status_change_status" to ParticleControlerDataBuffers.intArray( - intArrayOf(if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount) - ) - ) - } - - private fun buildChangeSingleStatusArgs( - index: Int, - status: Boolean, - ): Pair> { - return "index_status_change" to ParticleControlerDataBuffers.intArray( - intArrayOf( - index, - if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount - ) - ) - } - - open fun beforeDisplay(styles: SortedMap) {} - fun toggleScale(locations: SortedMap) { - super.toggleScale(locations.toMap()) - } - - override fun scale(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale can not be less than zero") - return - } - scale = new - if (displayed) { - toggleScaleDisplayed() - } - } - - override fun display(pos: Vec3, world: Level) { - if (displayed) { - return - } - displayed = true - this.pos = pos - this.world = world - this.client = world.isClientSide - if (!client) { - // 服务器只负责数据同步 不负责粒子生成 - onDisplay() - return - } - flush() - toggleDataStatus() - onDisplay() - } - - /** 清空粒子生成状态 重新生成粒子 */ - override fun flush() { - if (particles.isNotEmpty()) { - clear(true) - clearStatus() - } - displayParticles() - } - - override fun clear(valid: Boolean) { - sequencedParticles.clear() - super.clear(valid) - } - - /** 新的参数用于传输 粒子启用顺序 */ - override fun writePacketArgs(): Map> { - return mapOf( - "status" to ParticleControlerDataBuffers.longArray(displayedStatus), - "displayed_particle_count" to ParticleControlerDataBuffers.int(displayedParticleCount), - *writePacketArgsSequenced().map { it.key to it.value }.toTypedArray() - ) - } - - override fun readPacketArgs(args: Map>) { - args["status"]?.let { - val array = it.loadedValue as LongArray - System.arraycopy(array, 0, displayedStatus, 0, array.size) - } - args["displayed_particle_count"]?.let { displayedParticleCount = it.loadedValue!! as Int } - readPacketArgsSequenced(args) - args["index_status_change"]?.let { - val array = it.loadedValue as IntArray - - /** @see particleLinkageDisplayCurrentIndex */ - val index = array[0] - val status = array[1] == CREATE_PARTICLE - this.displayedParticleCount = array[2] - changeSingleStatus(index, status) - } - - args["indexes_status_change_arg"]?.let { - val indexes = it.loadedValue as IntArray - val methodArray = args["indexes_status_change_status"]!!.loadedValue as IntArray - val status = methodArray[0] == CREATE_PARTICLE - this.displayedParticleCount = methodArray[1] - changeParticlesStatus(indexes, status) - } - } - - fun displayParticles() { - val locations = getCurrentFramesSequenced() - beforeDisplay(locations) - toggleScale(locations) - sequencedParticles.addAll(locations.toList()) - Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) - } - - override fun rotateAsAxis(angle: Double) { - Math3DUtil.rotateAsAxis( - sequencedParticles.map { it.second }.toList(), axis, angle - ) - this.rotate += angle - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client && !autoToggle) { - // 同步到其他客户端 - change( - mapOf( - "rotate_angle" to ParticleControlerDataBuffers.double(angle) - ) - ) - } - } - - override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { - Math3DUtil.rotateAsAxis( - sequencedParticles.map { it.second }.toList(), axis, angle - ) - Math3DUtil.rotatePointsToPoint( - sequencedParticles.map { it.second }.toList(), to, axis - ) - axis = to - this.rotate += angle - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client && !autoToggle) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to), - "rotate_angle" to ParticleControlerDataBuffers.double(angle) - ) - ) - } - } - - override fun rotateToPoint(to: RelativeLocation) { - Math3DUtil.rotatePointsToPoint( - sequencedParticles.map { it.second }.toList(), to, axis - ) - axis = to - toggleRelative() - if (!client && !autoToggle) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to) - ) - ) - } - } - - final override fun toggleScaleDisplayed() { - sequencedParticles.forEach { - val uuid = it.first.uuid - val len = particleDefaultLength[uuid]!! - val value = it.second - if (len in -1e-3..1e-3) return@forEach - value.multiply(len * scale / value.length()) - } - toggleRelative() - } - - private fun createWithIndex(index: Int) { - if (!client) { - return - } - - if (index !in 0..sequencedParticles.size - 1) { - return - } - - val (data, rl) = sequencedParticles[index] - val uuid = data.uuid - - val displayer = data.displayerBuilder(uuid) - prepareCParticleDisplayer(displayer, sequencedParticles.size) - if (displayer is ParticleDisplayer.SingleParticleDisplayer) { - val controler = ControlParticleManager.createControl(uuid) - controler.applyInitializedAction(data.particleHandler) - } - val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) - val controler = displayer.display(toPos, world as ClientLevel) ?: return - if (controler is ParticleControler) { - data.particleControlerHandler(controler) - } - registerCParticleNode(controler) - particles[uuid] = controler - particleLocations[controler] = rl - } - - private fun toggleFromStatus(index: Int, status: Boolean) { - if (index >= sequencedParticles.size && client || index > getParticlesCount()) return - if (index < 0) return - if (!client) return - if (status) { - createWithIndex(index) - } else { - val uuid = sequencedParticles[index].first.uuid - val particle = particles[uuid] ?: return - unregisterCParticleNode(particle) - particle.remove() - particles.remove(uuid) - particleLocations.remove(particle) - } - setStatus(index, status) - } - - private fun setStatus(index: Int, status: Boolean) { - val page = MathDataUtil.getStoragePageLong(index) - val container = displayedStatus[page] - val bit = MathDataUtil.getStorageWithBitLong(index) - displayedStatus[page] = MathDataUtil.setStatusLong(container, bit, status) - } - - private fun getStatus(index: Int): Boolean { - val page = MathDataUtil.getStoragePageLong(index) - val container = displayedStatus[page] - val bit = MathDataUtil.getStorageWithBitLong(index) - return MathDataUtil.getStatusLong(container, bit) == 1 - } - - /** 在同步了data状态后 执行生成已经生成的 客户端执行 */ - private fun toggleDataStatus() { - if (!client) { - return - } - - if (displayedStatus.isEmpty()) return - if (!displayed) return - displayedStatus.forEachIndexed { page, container -> - for (bit in 1..64) { - val index = page * 64 + bit - 1 - if (index >= sequencedParticles.size) { - break - } - val status = MathDataUtil.getStatusLong(container, bit) - toggleFromStatus(index, status == 1) - } - - } - } - - private fun clearStatus() { - for (i in displayedStatus.indices) { - displayedStatus[i] = 0L - } - } - - final override fun getCurrentFrames(): Map { - return mapOf() - } - - - final override fun beforeDisplay(styles: Map) {} -} diff --git a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt b/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt deleted file mode 100644 index 428667b6..00000000 --- a/neoforge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt +++ /dev/null @@ -1,111 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.util - -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.core.particles.ParticleOptions -import net.minecraft.server.level.ServerLevel -import net.minecraft.world.phys.Vec3 - -object ServerParticleUtil { - - @JvmStatic - fun spawnBatch( - type: ParticleOptions, - world: ServerLevel, - positions: Iterable, - velocity: Vec3, - range: Double, - ) { - val allPositions = positions.toList() - if (allPositions.isEmpty()) return - world.players().forEach { player -> - val visiblePositions = allPositions.filter { player.position().distanceTo(it) <= range } - visiblePositions.chunked(PacketParticleBatchS2C.MAX_PARTICLES).forEach { batch -> - CooParticlesServices.SERVER_NETWORK.send(PacketParticleBatchS2C(type, batch, velocity), player) - } - } - } - - @JvmStatic - fun spawnBatch( - type: ParticleOptions, - world: ServerLevel, - positions: Iterable, - velocity: Vec3, - ) { - positions.toList() - .chunked(PacketParticleBatchS2C.MAX_PARTICLES) - .filter { it.isNotEmpty() } - .forEach { batch -> - val packet = PacketParticleBatchS2C(type, batch, velocity) - world.players().forEach { CooParticlesServices.SERVER_NETWORK.send(packet, it) } - } - } - /** - * 使用minecraft的 spawnParticle方法 - * 可能无法设置粒子移动方向 - */ - fun spawnSingle( - type: ParticleOptions, - world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int - ) { - world.players().forEach { - world.sendParticles( - it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed - ) - } - } - - /** - * 使用minecraft的 spawnParticle方法 - * 可能无法设置粒子移动方向 - */ - fun spawnSingle( - type: ParticleOptions, - world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int, range: Double - ) { - world.players().forEach { - if (it.position().distanceTo(pos) > range) { - return@forEach - } - world.sendParticles( - it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed - ) - } - } - - /** - * 使用CooParticleAPI的 spawnParticle方法 - */ - fun spawnSingle( - type: ParticleOptions, - world: ServerLevel, - pos: RelativeLocation, - velocity: RelativeLocation, - range: Double - ) { - spawnSingle(type, world, pos.toVector(), velocity.toVector(), range) - } - - fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: RelativeLocation, velocity: RelativeLocation) { - spawnSingle(type, world, pos.toVector(), velocity.toVector()) - } - - fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3, range: Double) { - world.players().forEach { - if (it.position().distanceTo(pos) > range) { - return@forEach - } - CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) - } - } - - fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3) { - world.players().forEach { - CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) - } - } - -} From 913435f1800d6f46e727d08f4cecc9e91696218b Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 20:50:04 +0800 Subject: [PATCH 17/41] fix: remove CustomPacketPayload from common interfaces and implement Forge networking --- .../annotations/codec/CodecHelper.kt | 106 ++-- .../annotations/codec/CommonStreamCodec.kt | 13 + .../packet/api/CooServerPacketManager.kt | 3 +- .../platform/ClientNetworking.kt | 10 +- .../platform/ServerNetworking.kt | 12 +- .../platform/network/ServerContext.kt | 3 +- forge/build.gradle | 83 +++ .../iris/FinalPassRendererAccessor.java | 6 + .../cooparticlesapi/CooParticlesAPIForge.kt | 59 ++ .../annotations/codec/ForgeCodecHelper.kt | 306 ++++++++++ .../annotations/codec/ForgeStreamCodec.kt | 13 + .../ParticleCompositionRegistryHelper.kt | 73 +++ .../handle/ParticleEmittersRegistryHelper.kt | 111 ++++ .../annotations/packet/CooPacketRegistry.kt | 95 +++ .../packet/CooPacketRegistryHelper.kt | 44 ++ .../api/controler/SerializableData.kt | 24 + .../cparticle/CParticleColorCurve.kt | 256 ++++++++ .../cparticle/CParticleCurve.kt | 201 ++++++ .../cparticle/CParticleTextureSource.kt | 232 +++++++ .../data/holder/DataHolderManager.kt | 81 +++ .../display/AutoDisplayEntity.kt | 12 + .../cooparticlesapi/display/DisplayEntity.kt | 292 +++++++++ .../display/DisplayEntityManager.kt | 239 ++++++++ .../cooparticlesapi/items/CooItemForge.kt | 21 + .../network/buffer/BooleanControlerBuffer.kt | 40 -- .../network/buffer/CharControlerBuffer.kt | 34 -- .../network/buffer/DoubleControlerBuffer.kt | 33 - .../network/buffer/EmptyControlerBuffer.kt | 36 -- .../network/buffer/FloatControlerBuffer.kt | 37 -- .../network/buffer/IntArrayControlerBuffer.kt | 45 -- .../network/buffer/IntControlerBuffer.kt | 35 -- .../buffer/LongArrayControlerBuffer.kt | 46 -- .../network/buffer/LongControlerBuffer.kt | 34 -- .../buffer/NestedBuffersControlerBuffer.kt | 71 --- .../buffer/ParticleControlerDataBuffer.kt | 32 - .../buffer/ParticleControlerDataBuffers.kt | 184 ------ .../buffer/RelativeLocationControlerBuffer.kt | 36 -- .../network/buffer/ShortControlerBuffer.kt | 33 - .../network/buffer/StringControlerBuffer.kt | 33 - .../network/buffer/UUIDControlerBuffer.kt | 33 - .../network/buffer/Vec3dControlerBuffer.kt | 41 -- .../network/packet/api/ClientContext.kt | 33 - .../packet/api/CooClientPacketManager.kt | 24 +- .../network/packet/api/CooPacket.kt | 5 +- .../network/packet/api/CooPacketKind.kt | 24 - .../packet/api/CooServerPacketManager.kt | 29 +- .../network/packet/api/ServerContext.kt | 33 - .../api/envelope/CooPacketEnvelopeC2S.kt | 10 +- .../api/envelope/CooPacketEnvelopeS2C.kt | 10 +- .../packet/client/PacketKeyActionC2S.kt | 60 +- .../listener/ClientCameraShakeHandler.kt | 80 --- .../listener/ClientClearStateHandler.kt | 13 - .../listener/ClientDataHolderPacketHandler.kt | 15 - .../ClientDisplayEntityPacketHandler.kt | 50 -- .../ClientDisplayEntityStateHandler.kt | 19 - .../ClientKeyBindingCountdownHandler.kt | 15 - .../ClientParticleCompositionHandler.kt | 89 --- .../ClientParticleCompositionRotateHandler.kt | 18 - .../ClientParticleCompositionStateHandler.kt | 20 - .../ClientParticleEmittersPacketHandler.kt | 75 --- .../ClientParticleGroupPacketHandler.kt | 126 ---- .../listener/ClientParticlePacketHandler.kt | 32 - .../ClientParticleStylePacketHandler.kt | 94 --- .../ClientRenderEntityPacketHandler.kt | 89 --- .../ClientRendererPostEffectHandler.kt | 41 -- .../listener/ClientSoundInstanceHandler.kt | 11 - .../client/listener/ClientSoundLoopHandler.kt | 11 - .../server/PacketBindStarfieldFboBlockS2C.kt | 67 -- .../packet/server/PacketCameraShakeS2C.kt | 83 +-- .../server/PacketClearClientStateS2C.kt | 7 +- .../server/PacketCooFxModelBlockTestS2C.kt | 6 - .../packet/server/PacketDataHolderS2C.kt | 96 ++- .../packet/server/PacketDisplayEntityS2C.kt | 33 +- .../server/PacketDisplayEntityStateS2C.kt | 37 +- .../server/PacketKeyBindingCountdownS2C.kt | 17 +- .../packet/server/PacketParticleBatchS2C.kt | 29 +- .../PacketParticleCompositionRotateS2C.kt | 27 +- .../server/PacketParticleCompositionS2C.kt | 47 +- .../PacketParticleCompositionStateS2C.kt | 41 +- .../server/PacketParticleEmittersS2C.kt | 52 +- .../packet/server/PacketParticleGroupS2C.kt | 108 ++-- .../packet/server/PacketParticleS2C.kt | 35 +- .../packet/server/PacketParticleStyleS2C.kt | 66 +- .../packet/server/PacketRenderEntityS2C.kt | 41 +- .../server/PacketRendererPostEffectS2C.kt | 29 +- .../packet/server/PacketSoundInstanceS2C.kt | 13 +- .../packet/server/PacketSoundLoopS2C.kt | 13 +- .../server/PacketTerrainEffectGroupS2C.kt | 128 +--- .../packet/server/PacketTerrainMappingS2C.kt | 19 +- .../server/listener/ServerKeyActionHandler.kt | 17 - .../PacketPerformanceStatusControlS2C.kt | 38 -- .../PacketPerformanceStatusRequestC2S.kt | 39 -- .../PacketPerformanceStatusSnapshotS2C.kt | 191 ------ .../PacketOpenBoundTestSelectionScreenS2C.kt | 48 -- .../PacketOpenTestControllerScreenS2C.kt | 123 ---- .../PacketReviewTestControllerC2S.kt | 46 -- .../PacketSelectBoundTestControllerC2S.kt | 31 - .../testblock/PacketStartTestControllerC2S.kt | 31 - .../testblock/PacketStopTestControllerC2S.kt | 31 - .../PacketUpdateTestControllerC2S.kt | 80 --- .../particle/SequencedServerParticleGroup.kt | 164 ----- .../network/particle/ServerParticleGroup.kt | 328 ---------- .../particle/ServerParticleGroupManager.kt | 200 ------ .../composition/AutoParticleComposition.kt | 7 +- .../AutoSequencedParticleComposition.kt | 11 +- .../particle/composition/CompositionData.kt | 51 -- .../composition/ParticleComposition.kt | 190 +----- .../composition/ParticleShapeComposition.kt | 29 +- .../SequencedParticleComposition.kt | 515 +--------------- .../SequencedParticleShapeComposition.kt | 198 +----- .../manager/ParticleCompositionManager.kt | 54 +- .../network/particle/data/DoubleRangeData.kt | 4 +- .../network/particle/data/FloatRangeData.kt | 2 +- .../network/particle/data/IntRangeData.kt | 4 +- .../network/particle/data/RangeData.kt | 4 +- .../network/particle/emitters/AutoEmitters.kt | 16 +- .../particle/emitters/AutoParticleEmitters.kt | 19 +- .../AutoTransformableCParticleEmitter.kt | 17 +- .../emitters/CParticleEmitterSpace.kt | 34 -- .../particle/emitters/ClassEmitters.kt | 75 +-- .../emitters/ClassParticleEmitters.kt | 183 +----- .../emitters/CompositionEmittersData.kt | 142 ++--- .../emitters/ControlableCParticleData.kt | 286 ++------- .../emitters/ControlableParticleData.kt | 459 +++----------- .../emitters/DisplayEntityEmittersData.kt | 165 ++--- .../particle/emitters/ParticleDataFactory.kt | 5 - .../particle/emitters/ParticleEmitters.kt | 43 +- .../emitters/ParticleEmittersManager.kt | 36 +- .../particle/emitters/PhysicConstant.kt | 23 - .../emitters/SimpleRandomParticleData.kt | 32 +- .../emitters/TransformableCParticleEmitter.kt | 488 ++++----------- .../particle/emitters/command/OrbitMode.kt | 30 - .../command/ParticleAttractionCommand.kt | 128 ---- .../emitters/command/ParticleCommand.kt | 46 -- .../emitters/command/ParticleCommandQueue.kt | 120 ---- .../command/ParticleDistortionCommand.kt | 334 ---------- .../emitters/command/ParticleDragCommand.kt | 99 --- .../command/ParticleFlowFieldCommand.kt | 119 ---- .../command/ParticleGravityCommand.kt | 20 - .../emitters/command/ParticleInheritMode.kt | 6 - .../command/ParticleInheritVelocityCommand.kt | 340 ----------- .../command/ParticleLifetimeMotionCommand.kt | 377 ------------ .../command/ParticleLifetimeVelocityMode.kt | 7 - .../emitters/command/ParticleMotionSpace.kt | 6 - .../emitters/command/ParticleNoiseCommand.kt | 205 ------- .../emitters/command/ParticleOrbitCommand.kt | 177 ------ .../command/ParticleRotationForceCommand.kt | 98 --- .../ParticleToroidalCirculationCommand.kt | 226 ------- .../emitters/command/ParticleVortexCommand.kt | 161 ----- .../command/curve/BezierFloatKeyframe.kt | 10 - .../command/curve/BezierKeyframeFloatCurve.kt | 250 -------- .../command/curve/ConstantFloatCurve.kt | 18 - .../emitters/command/curve/FloatCurve.kt | 22 - .../emitters/command/curve/FloatKeyframe.kt | 12 - .../command/curve/KeyframeFloatCurve.kt | 78 --- .../command/curve/LinearFloatCurve.kt | 27 - .../environment/wind/BallWindDirection.kt | 76 +-- .../environment/wind/BoxWindDirection.kt | 91 +-- .../environment/wind/GlobalWindDirection.kt | 67 +- .../environment/wind/WindDirection.kt | 82 +-- .../environment/wind/WindDirections.kt | 69 +-- .../emitters/event/ParticleCollideEvent.kt | 25 - .../particle/emitters/event/ParticleEvent.kt | 24 - .../emitters/event/ParticleEventHandler.kt | 26 - .../event/ParticleEventHandlerManager.kt | 56 -- .../emitters/event/ParticleHitEntityEvent.kt | 23 - .../emitters/event/ParticleOnGroundEvent.kt | 34 -- .../emitters/event/ParticleOnLiquidEvent.kt | 24 - .../emitters/type/BoxEmittersShootType.kt | 77 +-- .../emitters/type/EmittersShootType.kt | 72 ++- .../emitters/type/EmittersShootTypes.kt | 89 +-- .../emitters/type/LineEmittersShootType.kt | 52 +- .../emitters/type/MathEmittersShootType.kt | 195 +----- .../emitters/type/PointEmittersShootType.kt | 47 +- .../particle/style/ParticleGroupStyle.kt | 573 ------------------ .../particle/style/ParticleShapeStyle.kt | 222 ------- .../particle/style/ParticleStyleManager.kt | 259 -------- .../particle/style/ParticleStyleProvider.kt | 13 - .../style/SequencedParticleShapeStyle.kt | 288 --------- .../particle/style/SequencedParticleStyle.kt | 551 ----------------- .../particle/util/ServerParticleUtil.kt | 111 ---- .../particles/ControlableParticleEffect.kt | 16 + .../particles/CooModParticles.kt | 335 ++++++++++ .../particles/ParticleCameraOption.kt | 49 ++ .../impl/SimpleControlableParticleEffect.kt | 68 +++ .../platform/ClientNetworking.kt | 5 - .../platform/ForgeClientNetworking.kt | 11 + .../platform/ForgeNetworkChannel.kt | 22 +- .../platform/ForgePlatformHelper.kt | 9 + .../cooparticlesapi/platform/ForgeRegistry.kt | 52 ++ .../platform/ForgeServerNetworking.kt | 46 ++ .../platform/ServerNetworking.kt | 15 - .../platform/network/ForgeClientContext.kt | 13 + .../platform/network/ForgeServerContext.kt | 19 + .../platform/services/IPlatformHelper.kt | 7 + .../renderer/AutoRenderEntity.kt | 18 + .../cooparticlesapi/renderer/RenderEntity.kt | 232 +++++++ .../renderer/pipeline/CooUniformValue.kt | 263 ++++++++ .../renderer/post/PostEffectParams.kt | 194 ++++++ .../runtime/ClientRenderEntityRegistry.kt | 82 +++ .../runtime/ClientRenderEntityType.kt | 9 + .../runtime/RenderEntityAutoRegistry.kt | 302 +++++++++ .../terrain/CooTerrainMappingRegion.kt | 237 ++++++++ .../interpolator/data/InterpolatorDouble.kt | 74 +++ .../interpolator/data/InterpolatorFloat.kt | 74 +++ .../data/InterpolatorQuaternionf.kt | 34 ++ .../data/InterpolatorRelativeLocation.kt | 33 + .../interpolator/data/InterpolatorVec3d.kt | 54 ++ .../interpolator/data/InterpolatorVector3f.kt | 58 ++ forge/src/main/resources/META-INF/mods.toml | 26 + ....cooparticlesapi.platform.ClientNetworking | 1 + ...stack.cooparticlesapi.platform.CooRegistry | 1 + ....cooparticlesapi.platform.ServerNetworking | 1 + ...iclesapi.platform.services.IPlatformHelper | 1 + settings.gradle | 3 +- 215 files changed, 5524 insertions(+), 12479 deletions(-) create mode 100644 common/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CommonStreamCodec.kt create mode 100644 forge/build.gradle create mode 100644 forge/src/main/java/cn/coostack/cooparticlesapi/mixin/compat/iris/FinalPassRendererAccessor.java create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/CooParticlesAPIForge.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeCodecHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/ForgeStreamCodec.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/composition/handler/ParticleCompositionRegistryHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/emitter/handle/ParticleEmittersRegistryHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistry.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/packet/CooPacketRegistryHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleCurve.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleTextureSource.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/items/CooItemForge.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ParticleCameraOption.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeClientNetworking.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgePlatformHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeRegistry.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeServerNetworking.kt delete mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeClientContext.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/network/ForgeServerContext.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/services/IPlatformHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/pipeline/CooUniformValue.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityRegistry.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityType.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/RenderEntityAutoRegistry.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/terrain/CooTerrainMappingRegion.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorDouble.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorFloat.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorQuaternionf.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorRelativeLocation.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVec3d.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/utils/interpolator/data/InterpolatorVector3f.kt create mode 100644 forge/src/main/resources/META-INF/mods.toml create mode 100644 forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ClientNetworking create mode 100644 forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.CooRegistry create mode 100644 forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.ServerNetworking create mode 100644 forge/src/main/resources/META-INF/services/cn.coostack.cooparticlesapi.platform.services.IPlatformHelper 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..1abcec80 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(BlockPos.STREAM_CODEC::encode, BlockPos.STREAM_CODEC::decode) ) 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,7 +490,7 @@ object CodecHelper { } @Suppress("UNCHECKED_CAST") - fun codecList(type: Type): StreamCodec { + fun codecList(type: Type): CommonStreamCodec<*> { if (type !is ParameterizedType) { throw IllegalArgumentException("List字段必须声明具体泛型: $type") } @@ -519,7 +517,7 @@ object CodecHelper { } @Suppress("UNCHECKED_CAST") - fun codecSet(type: Type): StreamCodec { + fun codecSet(type: Type): CommonStreamCodec<*> { if (type !is ParameterizedType) { throw IllegalArgumentException("Set字段必须声明具体泛型: $type") } @@ -546,7 +544,7 @@ object CodecHelper { } @Suppress("UNCHECKED_CAST") - fun codecMap(type: Type): StreamCodec { + fun codecMap(type: Type): CommonStreamCodec<*> { if (type !is ParameterizedType) { throw IllegalArgumentException("Map字段必须声明具体泛型: $type") } @@ -585,7 +583,7 @@ 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>( @@ -612,7 +610,7 @@ 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>( @@ -641,7 +639,7 @@ 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 val valueCodec = registryCodecOf(type.actualTypeArguments[1]) as 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/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/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..a31ab462 --- /dev/null +++ b/forge/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'multiloader-loader' + id 'net.minecraftforge.gradle' +} + +tasks.javadoc { + enabled = false +} + +tasks.named('processResources', ProcessResources) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +minecraft { + version = forge_version + mappings channel: 'parchment', version: "${parchment_minecraft}-${parchment_version}" + 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 current = tasks.named("sourcesJar").get() + dependsOn(current) + from current.outputs.files.collect { zipTree(it) } + def other = project(":common").tasks.named("sourcesJar") + dependsOn(other) + from zipTree(other.get().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 { + 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" + additionalRuntimeClasspath 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/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/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/api/controler/SerializableData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt new file mode 100644 index 00000000..24aab070 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt @@ -0,0 +1,24 @@ +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.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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..26d768e9 --- /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 net.minecraft.network.codec.StreamCodec +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..3b1307cf --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt @@ -0,0 +1,81 @@ +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.network.codec.StreamCodec +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: StreamCodec<*, *>) { + registeredTypes[type.name] = codec + } + + fun getCodecFromID(id: String): StreamCodec<*, *>? { + 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): StreamCodec<*, *> { + 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..4900f8be --- /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.FriendlyByteBuf +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..26061ce8 --- /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.FriendlyByteBuf +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: FriendlyByteBuf) { + 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: FriendlyByteBuf) { + 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..9ba99a4d --- /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.FriendlyByteBuf +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 = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) + 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/buffer/BooleanControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt deleted file mode 100644 index 0c548747..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/BooleanControlerBuffer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class BooleanControlerBuffer() : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "boolean" - ) - ) - } - - override var loadedValue: Boolean? = false - - override fun encode(value: Boolean): ByteArray { - return byteArrayOf(if (value) 1 else 0) - } - - - override fun encode(): ByteArray { - return encode(loadedValue ?: false) - } - - override fun decode(buf: ByteArray): Boolean { - if (buf.isNotEmpty()) { - return buf[0] == 1.toByte() - } - return false - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt deleted file mode 100644 index 9fd449e4..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/CharControlerBuffer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class CharControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "char" - ) - ) - } - override var loadedValue: Char? = ' ' - override fun encode(value: Char): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Char { - return String(buf)[0] - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt deleted file mode 100644 index 70e91e64..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/DoubleControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class DoubleControlerBuffer() : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "double" - ) - ) - } - override var loadedValue: Double? = 0.0 - override fun encode(value: Double): ByteArray { - return value.toString().toByteArray() - } - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Double { - return String(buf).toDouble() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt deleted file mode 100644 index fa82448f..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/EmptyControlerBuffer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class EmptyControlerBuffer() : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "empty" - ) - ) - } - - override var loadedValue: Unit? = null - - override fun encode(value: Unit): ByteArray { - return byteArrayOf(0) - } - - - override fun encode(): ByteArray { - return byteArrayOf() - } - - override fun decode(buf: ByteArray) { - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt deleted file mode 100644 index f538f1f4..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/FloatControlerBuffer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class FloatControlerBuffer() : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "float" - ) - ) - } - - override var loadedValue: Float? = 0.0f - override fun encode(value: Float): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Float { - return String(buf).toFloat() - - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt deleted file mode 100644 index 72236692..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntArrayControlerBuffer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - -class IntArrayControlerBuffer : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "int_array" - ) - ) - } - - override var loadedValue: IntArray? = IntArray(0) - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun encode(value: IntArray): ByteArray { - val buffer = Unpooled.buffer() - buffer.writeInt(value.size) - value.forEach { buffer.writeInt(it) } - return buffer.copy().array() - } - - override fun decode(buf: ByteArray): IntArray { - val wrap = Unpooled.wrappedBuffer(buf) - val size = wrap.readInt() - val arr = IntArray(size) - for (i in 0 until size) { - arr[i] = wrap.readInt() - } - return arr - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt deleted file mode 100644 index d2e89cf3..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/IntControlerBuffer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class IntControlerBuffer : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "int" - ) - ) - } - - override var loadedValue: Int? = 0 - override fun encode(value: Int): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Int { - return String(buf).toInt() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt deleted file mode 100644 index fcf69566..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongArrayControlerBuffer.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - -class LongArrayControlerBuffer : ParticleControlerDataBuffer { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "long_array" - ) - ) - } - - override var loadedValue: LongArray? = LongArray(0) - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun encode(value: LongArray): ByteArray { - val buffer = Unpooled.buffer() - buffer.writeInt(value.size) - value.forEach { - buffer.writeLong(it) - } - return buffer.copy().array() - } - - override fun decode(buf: ByteArray): LongArray { - val wrap = Unpooled.wrappedBuffer(buf) - val size = wrap.readInt() - val arr = LongArray(size) - for (i in 0 until size) { - arr[i] = wrap.readLong() - } - return arr - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt deleted file mode 100644 index c968c112..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/LongControlerBuffer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - - -class LongControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "long" - ) - ) - } - - override var loadedValue: Long? = 0L - override fun encode(value: Long): ByteArray { - return value.toString().toByteArray() - } - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Long { - return String(buf).toLong() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt deleted file mode 100644 index 4f40c30b..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/NestedBuffersControlerBuffer.kt +++ /dev/null @@ -1,71 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - - -/** - * 如果某个style使用了嵌套 (内容使用了其他Style或者group) - * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer - * 如果你使用了自定义的ParticleControlableDataBuffer - * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe - */ -class NestedBuffersControlerBuffer : ParticleControlerDataBuffer>> { - - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "nested" - ) - ) - } - - override var loadedValue: Map>? = null - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun encode(value: Map>): ByteArray { - val buf = Unpooled.buffer(value.size * 8) - // 第一个存 接下来的参数个数 - // 第一个 int代表key的长度 - // 第二个 int代表value类型的字符串类的长度 - // 第三个 int代表value的长度 - buf.writeInt(value.size) - value.forEach { (key, value) -> - buf.writeInt(key.length) - buf.writeBytes(key.toByteArray()) - val id = "${value.getBufferID().value.namespace}:${value.getBufferID().value.path}" - buf.writeInt(id.length) - buf.writeBytes(id.toByteArray()) - val subBytes = value.encode()!! - buf.writeInt(subBytes.size) - buf.writeBytes(subBytes) - } - return buf.copy().array() - } - - override fun decode(buf: ByteArray): Map> { - val buf = Unpooled.copiedBuffer(buf) - val len = buf.readInt() - val res = mutableMapOf>() - for (i in 0 until len) { - val keyLen = buf.readInt() - val key = String(buf.readBytes(keyLen).copy().array()) - val typeLen = buf.readInt() - val idSplit = String(buf.readBytes(typeLen).copy().array()).split(":") - val id = ResourceLocation.fromNamespaceAndPath(idSplit[0], idSplit[1]) - val valueLen = buf.readInt() - val valueBytes = buf.readBytes(valueLen).copy().array() - val buffer = ParticleControlerDataBuffers.withIdDecode(id, valueBytes)!! - res[key] = buffer - } - return res - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt deleted file mode 100644 index b18ca163..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import net.minecraft.resources.ResourceLocation - - -interface ParticleControlerDataBuffer { - - data class Id(val value: ResourceLocation) { - companion object { - @JvmStatic - fun toID(string: String): Id { - val split = string.split(":") - if (split.size != 2) { - throw IllegalArgumentException("Invalid ID format: $string") - } - val namespace = split[0] - val id = split[1] - return Id(ResourceLocation.fromNamespaceAndPath(namespace, id)) - } - } - } - - var loadedValue: T? - fun encode(): ByteArray? - - fun encode(value: T): ByteArray - - fun decode(buf: ByteArray): T - - fun getBufferID(): Id - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt deleted file mode 100644 index acdc1c72..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ParticleControlerDataBuffers.kt +++ /dev/null @@ -1,184 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -object ParticleControlerDataBuffers { - private val wrapperToPrimitive = mapOf, Class<*>>( - java.lang.Integer::class.java to Int::class.java, - java.lang.Double::class.java to Double::class.java, - java.lang.Long::class.java to Long::class.java, - java.lang.Float::class.java to Float::class.java, - java.lang.Boolean::class.java to Boolean::class.java, - java.lang.Character::class.java to Char::class.java, - java.lang.Byte::class.java to Byte::class.java, - java.lang.Short::class.java to Short::class.java - ) - val registerBuilder = HashMap>>() - val registerTypes = HashMap, ParticleControlerDataBuffer.Id>() - - fun boolean(value: Boolean): BooleanControlerBuffer = BooleanControlerBuffer().apply { loadedValue = (value) } - fun char(value: Char): CharControlerBuffer = CharControlerBuffer().apply { loadedValue = (value) } - fun string(value: String): StringControlerBuffer = StringControlerBuffer().apply { loadedValue = (value) } - fun int(value: Int): IntControlerBuffer = IntControlerBuffer().apply { loadedValue = (value) } - fun intArray(value: IntArray): IntArrayControlerBuffer = IntArrayControlerBuffer().apply { loadedValue = (value) } - fun double(value: Double): DoubleControlerBuffer = DoubleControlerBuffer().apply { loadedValue = (value) } - fun float(value: Float): FloatControlerBuffer = FloatControlerBuffer().apply { loadedValue = (value) } - fun long(value: Long): LongControlerBuffer = LongControlerBuffer().apply { loadedValue = (value) } - fun longArray(value: LongArray): LongArrayControlerBuffer = - LongArrayControlerBuffer().apply { loadedValue = (value) } - - fun short(value: Short): ShortControlerBuffer = ShortControlerBuffer().apply { loadedValue = (value) } - fun vec3d(value: Vec3): Vec3dControlerBuffer = Vec3dControlerBuffer().apply { loadedValue = (value) } - fun relative(value: RelativeLocation): RelativeLocationControlerBuffer = - RelativeLocationControlerBuffer().apply { loadedValue = (value) } - - fun uuid(value: UUID): UUIDControlerBuffer = UUIDControlerBuffer().apply { loadedValue = (value) } - fun empty(): EmptyControlerBuffer = EmptyControlerBuffer() - - /** - * 如果某个style使用了嵌套 (内容使用了其他Style或者group) - * 制作数据同步时 可以在最外层style加入 subGroup的 data buffer - * 如果你使用了自定义的ParticleControlableDataBuffer - * 那么你的buffer一定要通过 ParticleControlableDataBuffer.register(id, buffer::class.java)注册类型 否则会出现npe - */ - fun nested(value: Map>): NestedBuffersControlerBuffer = - NestedBuffersControlerBuffer().apply { loadedValue = value } - - fun withType(value: Any, clazz: Class>): ParticleControlerDataBuffer<*> { - val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - bufferCodec.loadedValue = value - return bufferCodec - } - - fun fromBufferType(value: Any, clazz: Class<*>): ParticleControlerDataBuffer<*>? { - val targetClass = registerTypes[clazz]?.let { clazz } - ?: wrapperToPrimitive[clazz]?.takeIf { registerTypes.containsKey(it) } - ?: return null - - return registerTypes[targetClass]?.let { withId(it, value) } - } - - fun withDecode(buf: ByteArray, clazz: Class>): ParticleControlerDataBuffer<*> { - val bufferCodec = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - bufferCodec.loadedValue = bufferCodec.decode(buf) - return bufferCodec - } - - - fun withId(id: ParticleControlerDataBuffer.Id, value: Any): ParticleControlerDataBuffer<*>? { - val clazz = registerBuilder[id] ?: return null - return withType(value, clazz) - } - - fun withIdDecode(id: ParticleControlerDataBuffer.Id, array: ByteArray): ParticleControlerDataBuffer<*>? { - val clazz = registerBuilder[id] ?: return null - return withDecode(array, clazz) - } - - fun withId(id: ResourceLocation, value: Any): ParticleControlerDataBuffer<*>? { - val identifier = ParticleControlerDataBuffer.Id(id) - val clazz = registerBuilder[identifier] ?: return null - return withType(value, clazz) - } - - fun withIdDecode(id: ResourceLocation, array: ByteArray): ParticleControlerDataBuffer<*>? { - val identifier = ParticleControlerDataBuffer.Id(id) - val clazz = registerBuilder[identifier] ?: return null - return withDecode(array, clazz) - } - - - fun register( - bufType: Class<*>, - id: ParticleControlerDataBuffer.Id, - type: Class> - ) { - registerBuilder[id] = type - registerTypes[bufType] = id - } - - fun encode(buffer: ParticleControlerDataBuffer): ByteArray { - val code = buffer.encode() ?: throw IllegalStateException("buffer encode value is null") - val decoderID = buffer::class.java.name - val byteBuf = Unpooled.buffer() - byteBuf.writeInt(decoderID.length) - byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) - byteBuf.writeBytes(code) - return byteBuf.copy().array() - } - - fun encode(value: T, buffer: ParticleControlerDataBuffer): ByteArray { - val code = buffer.encode(value) - val decoderID = buffer::class.java.name - val byteBuf = Unpooled.buffer() - byteBuf.writeInt(decoderID.length) - byteBuf.writeBytes(decoderID.toByteArray(Charsets.UTF_8)) - byteBuf.writeBytes(code) - return byteBuf.copy().array() - } - - fun decode(bytes: ByteArray): T { - // 要求目标编码器必须具有空构造函数 - val byteBuf = Unpooled.wrappedBuffer(bytes) - val len = byteBuf.readInt() - val toStringBytes = ByteArray(len) - byteBuf.readBytes(toStringBytes) - val decoderID = String(toStringBytes, Charsets.UTF_8) - val code = byteBuf.readBytes(byteBuf.readableBytes()) - val codeBytes = ByteArray(code.readableBytes()) - code.readBytes(codeBytes) - val clazz = Class.forName(decoderID) - val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - - return ins.decode(codeBytes) - } - - fun decodeToBuffer(bytes: ByteArray): ParticleControlerDataBuffer { - val buf = Unpooled.wrappedBuffer(bytes) - return decodeToBuffer(buf) - } - - fun decodeToBuffer(byteBuf: ByteBuf): ParticleControlerDataBuffer { - // 要求目标编码器必须具有空构造函数 - val len = byteBuf.readInt() - val toStringBytes = ByteArray(len) - byteBuf.readBytes(toStringBytes) - val decoderID = String(toStringBytes, Charsets.UTF_8) - val code = byteBuf.readBytes(byteBuf.readableBytes()) - val codeBytes = ByteArray(code.readableBytes()) - code.readBytes(codeBytes) - val clazz = Class.forName(decoderID) - val ins = clazz.getConstructor().newInstance() as ParticleControlerDataBuffer - - return ins.apply { loadedValue = (ins.decode(codeBytes)) } - } - - - init { - register(Boolean::class.java, BooleanControlerBuffer.id, BooleanControlerBuffer::class.java) - register(Long::class.java, LongControlerBuffer.id, LongControlerBuffer::class.java) - register(Integer::class.java, IntControlerBuffer.id, IntControlerBuffer::class.java) - register(Double::class.java, DoubleControlerBuffer.id, DoubleControlerBuffer::class.java) - register(Float::class.java, FloatControlerBuffer.id, FloatControlerBuffer::class.java) - register(String::class.java, StringControlerBuffer.id, StringControlerBuffer::class.java) - register(IntArray::class.java, IntArrayControlerBuffer.id, IntArrayControlerBuffer::class.java) - register(LongArray::class.java, LongArrayControlerBuffer.id, LongArrayControlerBuffer::class.java) - register(UUID::class.java, UUIDControlerBuffer.id, UUIDControlerBuffer::class.java) - register(Vec3::class.java, Vec3dControlerBuffer.id, Vec3dControlerBuffer::class.java) - register( - RelativeLocation::class.java, - RelativeLocationControlerBuffer.id, - RelativeLocationControlerBuffer::class.java - ) - register(Short::class.java, ShortControlerBuffer.id, ShortControlerBuffer::class.java) - register(Unit::class.java, EmptyControlerBuffer.id, EmptyControlerBuffer::class.java) - register(Char::class.java, CharControlerBuffer.id, CharControlerBuffer::class.java) - register(Map::class.java, NestedBuffersControlerBuffer.id, NestedBuffersControlerBuffer::class.java) - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt deleted file mode 100644 index 0e4ad1f6..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/RelativeLocationControlerBuffer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation - -class RelativeLocationControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "relative" - ) - ) - } - - override var loadedValue: RelativeLocation? = RelativeLocation() - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): RelativeLocation { - return RelativeLocation.fromBytes(buf) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - - override fun encode(value: RelativeLocation): ByteArray { - return value.toBytes() - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt deleted file mode 100644 index cb9f978e..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/ShortControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class ShortControlerBuffer() : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "short" - ) - ) - } - override var loadedValue: Short? = 0 - override fun encode(value: Short): ByteArray { - return value.toString().toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Short { - return String(buf).toShort() - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt deleted file mode 100644 index 70e63b35..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/StringControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation - -class StringControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "string" - ) - ) - } - override var loadedValue: String? = "" - override fun encode(value: String): ByteArray { - return value.toByteArray() - } - - - override fun encode(): ByteArray? { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): String { - return String(buf) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt deleted file mode 100644 index 2df22bab..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/UUIDControlerBuffer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import net.minecraft.resources.ResourceLocation -import java.util.UUID - -class UUIDControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "uuid" - ) - ) - } - override var loadedValue: UUID? = UUID.randomUUID() - override fun encode(value: UUID): ByteArray { - return value.toString().toByteArray() - } - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): UUID { - return UUID.fromString(String(buf)) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt deleted file mode 100644 index 8ff7da90..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/buffer/Vec3dControlerBuffer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package cn.coostack.cooparticlesapi.network.buffer - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import io.netty.buffer.Unpooled -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 - -class Vec3dControlerBuffer : ParticleControlerDataBuffer { - companion object { - @JvmStatic - val id = ParticleControlerDataBuffer.Id( - ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "vec3d" - ) - ) - } - - override var loadedValue: Vec3? = Vec3.ZERO - - override fun encode(): ByteArray { - return encode(loadedValue!!) - } - - override fun decode(buf: ByteArray): Vec3 { - val wrappedBuffer = Unpooled.wrappedBuffer(buf) - return Vec3(wrappedBuffer.readDouble(), wrappedBuffer.readDouble(), wrappedBuffer.readDouble()) - } - - override fun getBufferID(): ParticleControlerDataBuffer.Id { - return id - } - - override fun encode(value: Vec3): ByteArray { - val buf = Unpooled.buffer() - buf.writeDouble(value.x) - buf.writeDouble(value.y) - buf.writeDouble(value.z) - return buf.copy().array() - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt deleted file mode 100644 index 4113c61f..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ClientContext.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -import net.minecraft.client.Minecraft - -/** - * 客户端收到 [CooPacket] 时的上下文 - * - * @property packet 已解码的业务包 - * @property kind 包类型 (普通/请求/响应) - * @property correlationId 关联ID, 0 表示普通包 - * @property timeoutTicks 请求模式下的超时tick (来自发送端) - */ -class ClientContext( - val packet: CooPacket, - val kind: CooPacketKind, - val correlationId: Long, - val timeoutTicks: Int, -) { - val client: Minecraft = Minecraft.getInstance() - - /** - * 在请求模式下,向服务端回复一个响应包 - * - * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 - * - 使用本上下文携带的 [correlationId],服务端 request 注册的回调将匹配执行 - */ - fun reply(response: CooPacket) { - if (kind != CooPacketKind.REQUEST) { - throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") - } - CooClientPacketManager.replyInternal(response, correlationId) - } -} 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 index 7518244c..70eeda7a 100644 --- 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 @@ -10,7 +10,7 @@ import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelope 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.ForgeNetworkChannel +import cn.coostack.cooparticlesapi.platform.CooParticlesServices import net.minecraft.client.Minecraft import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong @@ -29,14 +29,6 @@ object CooClientPacketManager { ) private val pending = ConcurrentHashMap() - private lateinit var channel: ForgeNetworkChannel - - fun init(channel: ForgeNetworkChannel) { - this.channel = channel - channel.registerEnvelopeS2C { envelope -> - handleS2C(envelope) - } - } @JvmStatic fun sendTo(packet: CooPacket): Boolean { @@ -123,7 +115,7 @@ object CooClientPacketManager { val kind = CooPacketKind.fromId(envelope.kindId) val packet = CooPacketRegistry.decode(envelope.packetId, envelope.data) if (packet == null) { - CooParticlesConstants.logger.warn("收到未知 CooPacket: ${envelope.packetId} (kind=$kind)") + CooParticlesConstants.logger.warn("Received unknown CooPacket: ${envelope.packetId} (kind=$kind)") return } val event = CooEventBus.call( @@ -142,14 +134,14 @@ object CooClientPacketManager { try { packet.onClientReceive(ctx) } catch (e: Throwable) { - CooParticlesConstants.logger.error("CooPacket onClientReceive 异常: ${envelope.packetId}", e) + 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 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" + "CooPacket response type mismatch: expected ${pendingEntry.expectType.name}, got ${packet::class.java.name}" ) return } @@ -157,7 +149,7 @@ object CooClientPacketManager { pendingEntry.callback(packet) } catch (e: Throwable) { CooParticlesConstants.logger.error( - "CooPacket request 回调异常 (correlationId=${envelope.correlationId})", + "CooPacket request callback exception (correlationId=${envelope.correlationId})", e ) } @@ -176,7 +168,7 @@ object CooClientPacketManager { ): Boolean { if (!CooPacketRegistry.isRegistered(packet::class.java)) { CooParticlesConstants.logger.error( - "CooPacket 未注册, 无法发送: ${packet::class.java.name} (id=${packet.id()})" + "CooPacket not registered, cannot send: ${packet::class.java.name} (id=${packet.id()})" ) return false } @@ -194,7 +186,7 @@ object CooClientPacketManager { val data = try { CooPacketRegistry.encode(packet) } catch (e: Throwable) { - CooParticlesConstants.logger.error("CooPacket 编码失败: ${packet::class.java.name}", e) + CooParticlesConstants.logger.error("CooPacket encode failed: ${packet::class.java.name}", e) return false } val envelope = CooPacketEnvelopeC2S( @@ -204,7 +196,7 @@ object CooClientPacketManager { timeoutTicks = timeoutTicks, data = data, ) - channel.sendEnvelopeC2S(envelope) + CooParticlesServices.CLIENT_NETWORK.send(envelope) PerformanceStatusNetworkMetrics.recordSent( PerformanceStatusNetworkEndpoint.CLIENT, data.size, 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 index f61e54f4..58e7df30 100644 --- 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 @@ -1,13 +1,14 @@ package cn.coostack.cooparticlesapi.network.packet.api import cn.coostack.cooparticlesapi.annotations.packet.CooPacketRegistryHelper -import cn.coostack.cooparticlesapi.network.packet.api.CommonCodec import net.minecraft.resources.ResourceLocation abstract class CooPacket { abstract fun id(): ResourceLocation - open fun codec(): CommonCodec = + + 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/CooPacketKind.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt deleted file mode 100644 index 63a52437..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/CooPacketKind.kt +++ /dev/null @@ -1,24 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -/** - * 数据包类型 - * - * - [NORMAL]: 普通包,无需响应 - * - [REQUEST]: 请求包,等待对端响应 - * - [RESPONSE]: 响应包,对应某个 REQUEST 的回执 - */ -enum class CooPacketKind { - NORMAL, - REQUEST, - RESPONSE; - - companion object { - private val values = entries.toTypedArray() - - fun fromId(id: Int): CooPacketKind { - return values.getOrNull(id) ?: NORMAL - } - } - - val id: Int get() = ordinal -} 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 index 30d33eb2..b41906b1 100644 --- 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 @@ -11,7 +11,7 @@ import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelope 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.ForgeNetworkChannel +import cn.coostack.cooparticlesapi.platform.CooParticlesServices import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import java.util.UUID @@ -33,14 +33,6 @@ object CooServerPacketManager { ) private val pending = ConcurrentHashMap() - private lateinit var channel: ForgeNetworkChannel - - fun init(channel: ForgeNetworkChannel) { - this.channel = channel - channel.registerEnvelopeC2S { envelope, player -> - handleC2S(envelope, player) - } - } @JvmStatic fun sendTo(player: ServerPlayer, packet: CooPacket): Boolean { @@ -79,10 +71,11 @@ object CooServerPacketManager { @JvmSynthetic inline fun request( + player: ServerPlayer, packet: CooPacket, timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, noinline onResponse: (R) -> Unit, - ): Long = request(packet, R::class.java, timeoutTicks, onResponse) + ): Long = request(player, packet, R::class.java, timeoutTicks, onResponse) @JvmStatic fun requestWithSender( @@ -155,7 +148,7 @@ object CooServerPacketManager { packet: CooPacket, timeoutTicks: Int = DEFAULT_TIMEOUT_TICKS, noinline onResponse: (ServerPlayer, R) -> Unit, - ): List = requestWorlds(worlds, packet, R::class.java, timeoutTicks, onResponse) + ): List = requestWorlds(worlds, R::class.java, timeoutTicks, onResponse) @JvmStatic fun cancelRequest(correlationId: Long): Boolean { @@ -211,7 +204,7 @@ object CooServerPacketManager { val packet = CooPacketRegistry.decode(envelope.packetId, envelope.data) if (packet == null) { CooParticlesConstants.logger.warn( - "收到未知 CooPacket: ${envelope.packetId} (kind=$kind, sender=${sender.gameProfile.name})" + "Received unknown CooPacket: ${envelope.packetId} (kind=$kind, sender=${sender.gameProfile.name})" ) return } @@ -231,14 +224,14 @@ object CooServerPacketManager { try { packet.onServerReceive(ctx) } catch (e: Throwable) { - CooParticlesConstants.logger.error("CooPacket onServerReceive 异常: ${envelope.packetId}", e) + 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 响应类型不匹配: 期望 ${pendingEntry.expectType.name}, 实际 ${packet::class.java.name}" + "CooPacket response type mismatch: expected ${pendingEntry.expectType.name}, got ${packet::class.java.name}" ) return } @@ -246,7 +239,7 @@ object CooServerPacketManager { pendingEntry.callback(sender, packet) } catch (e: Throwable) { CooParticlesConstants.logger.error( - "CooPacket request 回调异常 (correlationId=${envelope.correlationId})", + "CooPacket request callback exception (correlationId=${envelope.correlationId})", e ) } @@ -266,7 +259,7 @@ object CooServerPacketManager { ): Boolean { if (!CooPacketRegistry.isRegistered(packet::class.java)) { CooParticlesConstants.logger.error( - "CooPacket 未注册, 无法发送: ${packet::class.java.name} (id=${packet.id()})" + "CooPacket not registered, cannot send: ${packet::class.java.name} (id=${packet.id()})" ) return false } @@ -284,7 +277,7 @@ object CooServerPacketManager { val data = try { CooPacketRegistry.encode(packet) } catch (e: Throwable) { - CooParticlesConstants.logger.error("CooPacket 编码失败: ${packet::class.java.name}", e) + CooParticlesConstants.logger.error("CooPacket encode failed: ${packet::class.java.name}", e) return false } val envelope = CooPacketEnvelopeS2C( @@ -294,7 +287,7 @@ object CooServerPacketManager { timeoutTicks = timeoutTicks, data = data, ) - channel.sendTo(player, envelope) + CooParticlesServices.SERVER_NETWORK.send(envelope, player) PerformanceStatusNetworkMetrics.recordSent( PerformanceStatusNetworkEndpoint.SERVER, data.size, diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt deleted file mode 100644 index 5d1dd997..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/api/ServerContext.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.api - -import net.minecraft.server.level.ServerPlayer - -/** - * 服务端收到 [CooPacket] 时的上下文 - * - * @property sender 发送该包的玩家 - * @property packet 已解码的业务包 - * @property kind 包类型 (普通/请求/响应) - * @property correlationId 关联ID, 0 表示普通包 - * @property timeoutTicks 请求模式下的超时tick (来自发送端) - */ -class ServerContext( - val sender: ServerPlayer, - val packet: CooPacket, - val kind: CooPacketKind, - val correlationId: Long, - val timeoutTicks: Int, -) { - /** - * 在请求模式下,向客户端回复一个响应包 - * - * - 必须在 [kind] == [CooPacketKind.REQUEST] 时调用 - * - 使用本上下文携带的 [correlationId],客户端 request 注册的回调将匹配执行 - */ - fun reply(response: CooPacket) { - if (kind != CooPacketKind.REQUEST) { - throw IllegalStateException("CooPacket reply 只能在 REQUEST 上下文中使用, 当前: $kind") - } - CooServerPacketManager.replyInternal(sender, response, correlationId) - } -} 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 index 798a85eb..8a9a7e74 100644 --- 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 @@ -1,7 +1,6 @@ package cn.coostack.cooparticlesapi.network.packet.api.envelope import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation @@ -11,13 +10,8 @@ class CooPacketEnvelopeC2S( val correlationId: Long, val timeoutTicks: Int, val data: ByteArray, -) : CooPacket() { - +) { companion object { - private val ID = ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "coo_packet_envelope_c2s" - ) - @JvmStatic fun write(buf: PacketByteBuf, packet: CooPacketEnvelopeC2S) { buf.writeVarInt(packet.kindId) @@ -38,6 +32,4 @@ class CooPacketEnvelopeC2S( ) } } - - override fun id(): ResourceLocation = ID } 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 index e753501c..100bb0ac 100644 --- 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 @@ -1,7 +1,6 @@ package cn.coostack.cooparticlesapi.network.packet.api.envelope import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation @@ -11,13 +10,8 @@ class CooPacketEnvelopeS2C( val correlationId: Long, val timeoutTicks: Int, val data: ByteArray, -) : CooPacket() { - +) { companion object { - private val ID = ResourceLocation.fromNamespaceAndPath( - CooParticlesConstants.MOD_ID, "coo_packet_envelope_s2c" - ) - @JvmStatic fun write(buf: PacketByteBuf, packet: CooPacketEnvelopeS2C) { buf.writeVarInt(packet.kindId) @@ -38,6 +32,4 @@ class CooPacketEnvelopeS2C( ) } } - - override fun id(): ResourceLocation = ID } 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 index ea2dba24..7463af62 100644 --- 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 @@ -5,46 +5,42 @@ 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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.writeResourceLocation(entry.keyId) + buf.writeVarInt(entry.actions.size) + entry.actions.forEach { action -> + buf.writeInt(action.id) } - }, { 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)) + 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())) } - PacketKeyActionC2S(KeyActionBatch(entries)) - }) + val pressTick = buf.readInt() + val released = buf.readBoolean() + entries.add(KeyActionData(keyId, actions, pressTick, released)) + } + PacketKeyActionC2S(KeyActionBatch(entries)) + }) } - - return payloadID \ No newline at end of file +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt deleted file mode 100644 index 7d545a33..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientCameraShakeHandler.kt +++ /dev/null @@ -1,80 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketCameraShakeS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.utils.ClientCameraUtil -import net.minecraft.world.phys.Vec3 - -object ClientCameraShakeHandler { - fun receive( - payload: PacketCameraShakeS2C, - context: ClientContext - ) { - when (payload.operation) { - PacketCameraShakeS2C.CameraOperation.SHAKE -> handleShake(payload, context) - PacketCameraShakeS2C.CameraOperation.SET_OFFSET -> { - if (payload.instant) { - ClientCameraUtil.setOffsetNow(payload.position, payload.yawOffset, payload.pitchOffset) - } else { - ClientCameraUtil.setOffset(payload.position, payload.yawOffset, payload.pitchOffset) - } - } - - PacketCameraShakeS2C.CameraOperation.RESET_OFFSET -> { - if (payload.instant) { - ClientCameraUtil.resetOffsetNow() - } else { - ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) - } - } - - PacketCameraShakeS2C.CameraOperation.FORCE_POSITION -> { - if (payload.instant) { - ClientCameraUtil.setForcedCameraPositionNow(payload.position) - } else { - ClientCameraUtil.setForcedCameraPosition(payload.position) - } - } - - PacketCameraShakeS2C.CameraOperation.RESET_FORCE_POSITION -> { - if (payload.instant) { - ClientCameraUtil.resetForcedCameraPositionNow() - } else { - ClientCameraUtil.resetForcedCameraPosition() - } - } - - PacketCameraShakeS2C.CameraOperation.RESET_ALL -> { - if (payload.instant) { - ClientCameraUtil.resetAllNow() - } else { - ClientCameraUtil.stopShakeCamera() - ClientCameraUtil.setOffset(Vec3.ZERO, 0f, 0f) - ClientCameraUtil.resetForcedCameraPosition() - } - } - } - } - - private fun handleShake(payload: PacketCameraShakeS2C, context: ClientContext) { - val range = payload.range - val player = context.player() - var amplitude = payload.amplitude - var frequency = payload.frequency - if (range > 0) { - val distance = player.position().distanceTo(payload.origin) - if (distance > range) { - return - } - if (payload.attenuateByDistance) { - val attenuation = (1.0 - distance / range).coerceIn(0.0, 1.0) - amplitude *= attenuation - frequency *= attenuation - } - } - if (amplitude <= 0.0 || frequency <= 0.0) { - return - } - ClientCameraUtil.startShakeCamera(payload.tick, amplitude, frequency) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt deleted file mode 100644 index f0a997e6..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientClearStateHandler.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesAPIClient -import cn.coostack.cooparticlesapi.network.packet.server.PacketClearClientStateS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientClearStateHandler { - fun receive(payload: PacketClearClientStateS2C, context: ClientContext) { - context.client().execute { - CooParticlesAPIClient.clearTransientClientState() - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt deleted file mode 100644 index 6e803e15..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDataHolderPacketHandler.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.data.holder.DataHolderManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketDataHolderS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientDataHolderPacketHandler { - fun receive( - payload: PacketDataHolderS2C, - context: ClientContext - ) { - val entity = context.player().level().getEntity(payload.entityId) ?: return - DataHolderManager.applyClient(entity, payload) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt deleted file mode 100644 index e34cb2cd..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityPacketHandler.kt +++ /dev/null @@ -1,50 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.display.DisplayEntity -import cn.coostack.cooparticlesapi.display.DisplayEntityManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.PacketByteBuf - -object ClientDisplayEntityPacketHandler { - fun receive( - payload: PacketDisplayEntityS2C, - context: ClientContext - ) { - context.client().execute { - apply(payload, context) - } - } - - private fun apply(payload: PacketDisplayEntityS2C, context: ClientContext) { - if (payload.removed) { - DisplayEntityManager.clientView.remove(payload.uuid) - return - } - val new = decodeData(payload) - new.world = context.player().level() - val old = DisplayEntityManager.clientView[payload.uuid] ?: let { - // 新建 - DisplayEntityManager.addClient(new) - return - } - // 更新 - old.update(new) - } - - private fun decodeData(payload: PacketDisplayEntityS2C): DisplayEntity { - val data = payload.data - val type = payload.type - val codec = DisplayEntityManager.registeredTypes[type]!! - val new = codec.decode( - PacketByteBuf( - Unpooled.wrappedBuffer(data), - Minecraft.getInstance().player!!.registryAccess() - ) - ) - return new - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt deleted file mode 100644 index 980fe036..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientDisplayEntityStateHandler.kt +++ /dev/null @@ -1,19 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.display.DisplayEntityManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketDisplayEntityStateS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientDisplayEntityStateHandler { - fun receive(payload: PacketDisplayEntityStateS2C, context: ClientContext) { - context.client().execute { - DisplayEntityManager.clientView[payload.uuid]?.applyRemoteState( - payload.position, - payload.yaw, - payload.pitch, - payload.roll, - payload.scale, - ) - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt deleted file mode 100644 index 9c36c138..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientKeyBindingCountdownHandler.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.key.CooKeyBindingManager -import cn.coostack.cooparticlesapi.network.packet.server.PacketKeyBindingCountdownS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientKeyBindingCountdownHandler { - fun receive( - payload: PacketKeyBindingCountdownS2C, - context: ClientContext - ) { - CooKeyBindingManager.setCountdown(payload.key, payload.cd) - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt deleted file mode 100644 index 1ebeecce..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionS2C -import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.PacketByteBuf -import java.util.concurrent.ConcurrentHashMap - -object ClientParticleCompositionHandler { - private val missingCodecWarnings = ConcurrentHashMap.newKeySet() - private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() - - fun receive( - payload: PacketParticleCompositionS2C, - context: ClientContext - ) { - context.client().execute { - apply(payload, context) - } - } - - private fun apply(payload: PacketParticleCompositionS2C, context: ClientContext) { - val distanceRemove = payload.distanceRemove - // 优化新建 - var new: ParticleComposition? = null - val old = ParticleCompositionManager.clientView[payload.uuid] - if (!distanceRemove) { - // 新建 如果old为null 则直接作为新的添加在里面 - new = decodeData(payload) ?: return - if (new.controlUUID != payload.uuid && identityMismatchWarnings.add(payload.type)) { - CooParticlesConstants.logger.warn( - "Composition 网络 UUID 不一致,已采用数据包 UUID: type={}, packet={}, decoded={}", - payload.type, - payload.uuid, - new.controlUUID, - ) - } - new.controlUUID = payload.uuid - new.world = context.player().level() - } - if (old == null && !distanceRemove) { - ParticleCompositionManager.addClient(new!!) - // fix 当composition消散的时候 会抛出npe的问题 - return - } - val current = old ?: return - // new == null时 distanceRemove应该为true - if (distanceRemove) { - current.remove() - } else { - if (payload.recreate || current.canceled || !current.displayed) { - current.clear(true) - ParticleCompositionManager.addClient(new!!) - return - } - // new 不是null 且 old 不为null 更新old - current.update(new!!) - } - } - - private fun decodeData(payload: PacketParticleCompositionS2C): ParticleComposition? { - val data = payload.data - val type = payload.type - val codec = ParticleCompositionManager.registeredTypes[type] ?: run { - if (missingCodecWarnings.add(type)) { - CooParticlesConstants.logger.warn( - "收到未注册的 Composition codec: type={}, uuid={}", - type, - payload.uuid, - ) - } - return null - } - val buffer = PacketByteBuf( - Unpooled.wrappedBuffer(data), - Minecraft.getInstance().player!!.registryAccess() - ) - return try { - codec.decode(buffer) - } finally { - buffer.release() - } - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt deleted file mode 100644 index ac7c7f34..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionRotateHandler.kt +++ /dev/null @@ -1,18 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.extend.asRelative -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionRotateS2C -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientParticleCompositionRotateHandler { - fun receive( - payload: PacketParticleCompositionRotateS2C, - context: ClientContext - ) { - context.client().execute { - val composition = ParticleCompositionManager.clientView[payload.uuid] ?: return@execute - composition.applyRemoteRotation(payload.direction?.asRelative(), payload.rollDelta) - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt deleted file mode 100644 index 70b29589..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleCompositionStateHandler.kt +++ /dev/null @@ -1,20 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleCompositionStateS2C -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientParticleCompositionStateHandler { - fun receive(payload: PacketParticleCompositionStateS2C, context: ClientContext) { - context.client().execute { - ParticleCompositionManager.clientView[payload.uuid]?.applyRemoteState( - payload.position, - payload.visibleRange, - payload.scale, - payload.displayStatus, - payload.closedInterval, - payload.current, - ) - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt deleted file mode 100644 index 18cd3fa2..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleEmittersPacketHandler.kt +++ /dev/null @@ -1,75 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleEmittersS2C -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmittersManager -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft -import net.minecraft.network.PacketByteBuf -import java.util.concurrent.ConcurrentHashMap - -object ClientParticleEmittersPacketHandler { - private val missingCodecWarnings = ConcurrentHashMap.newKeySet() - private val identityMismatchWarnings = ConcurrentHashMap.newKeySet() - - fun receive( - payload: PacketParticleEmittersS2C, - context: ClientContext - ) { - context.client().execute { - when (payload.type) { - PacketParticleEmittersS2C.PacketType.CREATE -> handleCreate(payload, context) - PacketParticleEmittersS2C.PacketType.CHANGE -> handleChange(payload, context) - PacketParticleEmittersS2C.PacketType.REMOVE -> handleRemove(payload) - } - } - } - - private fun handleCreate(payload: PacketParticleEmittersS2C, context: ClientContext) { - val emitters = decode(payload) ?: return - ParticleEmittersManager.createClient(emitters, context.player().level()) - } - - private fun handleChange(payload: PacketParticleEmittersS2C, context: ClientContext) { - val emitters = decode(payload) ?: return - ParticleEmittersManager.changeClient(emitters, context.player().level()) - } - - private fun handleRemove(payload: PacketParticleEmittersS2C) { - ParticleEmittersManager.removeClient(payload.emitterUUID) - } - - private fun decode(payload: PacketParticleEmittersS2C): ParticleEmitters? { - val codec = ParticleEmittersManager.getCodecFromID(payload.emitterID) ?: run { - if (missingCodecWarnings.add(payload.emitterID)) { - CooParticlesConstants.logger.warn( - "收到未注册的 Emitter codec: id={}, uuid={}", - payload.emitterID, - payload.emitterUUID, - ) - } - return null - } - val buffer = PacketByteBuf( - Unpooled.wrappedBuffer(payload.emitterData), - Minecraft.getInstance().level!!.registryAccess(), - ) - return try { - codec.decode(buffer).also { emitters -> - if (emitters.uuid != payload.emitterUUID && identityMismatchWarnings.add(payload.emitterID)) { - CooParticlesConstants.logger.warn( - "Emitter 网络 UUID 不一致,已采用数据包 UUID: id={}, packet={}, decoded={}", - payload.emitterID, - payload.emitterUUID, - emitters.uuid, - ) - } - emitters.uuid = payload.emitterUUID - } - } finally { - buffer.release() - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt deleted file mode 100644 index 62939ddd..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleGroupPacketHandler.kt +++ /dev/null @@ -1,126 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.particles.control.group.ClientParticleGroupManager -import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup -import cn.coostack.cooparticlesapi.particles.control.group.SequencedParticleGroup -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.Minecraft -import net.minecraft.world.phys.Vec3 -import java.util.UUID - - -object ClientParticleGroupPacketHandler { - fun receive(packet: PacketParticleGroupS2C, context: ClientContext) { - // Group UUID - val uuid = packet.uuid - // 修改类型 - val type = packet.type - when (type) { - ControlType.CREATE -> handleCreate(uuid, packet.args) - ControlType.CHANGE -> handleChange(uuid, packet.args) - ControlType.REMOVE -> handleRemove(uuid) - } - } - - - private fun handleCreate( - groupUUID: UUID, - args: Map> - ) { - val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]?.loadedValue!! as Vec3 - val world = Minecraft.getInstance().level!! - val type = args[PacketParticleGroupS2C.PacketArgsType.GROUP_TYPE.ofArgs]!!.loadedValue as String - val currentTick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue as Int - val maxTick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue as Int - val scale = args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue as Double - val groupClass = Class.forName(type) - val builder = ClientParticleGroupManager.getBuilder(groupClass as Class)!! - val group = builder.createGroup(groupUUID, args) - if (group is SequencedParticleGroup) { - handleSequencedGroupArgs(group, args) - } - group.tick = currentTick - group.maxTick = maxTick - group.scale(scale) - group.display(pos, world) - ClientParticleGroupManager.addVisibleGroup(group) - } - - private fun handleChange( - groupUUID: UUID, - args: Map> - ) { - val targetGroup = ClientParticleGroupManager.getControlGroup(groupUUID) ?: return - val argKeys = args.keys - if (PacketParticleGroupS2C.PacketArgsType.POS.ofArgs in argKeys) { - val pos = args[PacketParticleGroupS2C.PacketArgsType.POS.ofArgs]!!.loadedValue!! as Vec3 - targetGroup.teleportGroupTo(pos) - } - if (PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs in argKeys) { - val to = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs]!!.loadedValue!! as Vec3 - targetGroup.rotateToPoint(RelativeLocation.of(to)) - } - if (PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs in argKeys) { - val angle = args[PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs]!!.loadedValue as Double - targetGroup.rotateAsAxis(angle) - } - if (PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs in argKeys) { - val axis = args[PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs]!!.loadedValue!! as Vec3 - targetGroup.axis = RelativeLocation.of(axis) - } - if (PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs in argKeys) { - val tick = args[PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs]!!.loadedValue!! as Int - targetGroup.tick = tick - } - if (PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs in argKeys) { - val tick = args[PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs]!!.loadedValue!! as Int - targetGroup.maxTick = tick - } - if (PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs in argKeys) { - val targetMethodName = - args[PacketParticleGroupS2C.PacketArgsType.INVOKE.ofArgs]!!.loadedValue!! as String - targetGroup::class.java.getDeclaredMethod(targetMethodName).apply { - isAccessible = true - }.invoke(targetGroup) - } - - if (PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs in argKeys) { - targetGroup.scale( - args[PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs]!!.loadedValue!! as Double - ) - } - if (targetGroup is SequencedParticleGroup) { - handleSequencedGroupArgs(targetGroup, args) - } - - val builder = ClientParticleGroupManager.getBuilder(targetGroup::class.java) ?: return - builder.changeGroup(targetGroup, args) - } - - private fun handleRemove(groupUUID: UUID) { - ClientParticleGroupManager.removeVisible(groupUUID) - } - - private fun handleSequencedGroupArgs( - group: SequencedParticleGroup, - args: Map> - ) { - if (args.containsKey("toggle")) { - group.toggle(args["toggle"]!!.loadedValue as Int) - } - if (args.containsKey("addCount")) { - group.addMultiple(args["addCount"]!!.loadedValue as Int) - } - if (args.containsKey("removeCount")) { - group.removeMultiple(args["removeCount"]!!.loadedValue as Int) - } - - if (args.containsKey("toggle_status")) { - group.toggleStatus(args["toggle_status"]!!.loadedValue as LongArray) - } - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt deleted file mode 100644 index e55ab4d1..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticlePacketHandler.kt +++ /dev/null @@ -1,32 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext - -object ClientParticlePacketHandler { - fun receive(packet: PacketParticleBatchS2C, context: ClientContext) { - val world = context.player().level() - packet.positions.forEach { position -> - world.addParticle( - packet.type, - true, - position.x, - position.y, - position.z, - packet.velocity.x, - packet.velocity.y, - packet.velocity.z, - ) - } - } - - fun receive(packet: PacketParticleS2C, context: ClientContext) { - val player = context.player() - val world = player.level() - world.addParticle( - packet.type, true, packet.pos.x, packet.pos.y, packet.pos.z, - packet.velocity.x, packet.velocity.y, packet.velocity.z, - ) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt deleted file mode 100644 index 66bacfc1..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientParticleStylePacketHandler.kt +++ /dev/null @@ -1,94 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C -import cn.coostack.cooparticlesapi.network.particle.style.ParticleGroupStyle -import cn.coostack.cooparticlesapi.network.particle.style.ParticleStyleManager -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.Minecraft -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -object ClientParticleStylePacketHandler { - fun receive(packet: PacketParticleStyleS2C, context: ClientContext) { - // Group UUID - val uuid = packet.uuid - // 修改类型 - val type = packet.type - when (type) { - ControlType.CREATE -> handleCreate(uuid, packet.args) - ControlType.CHANGE -> handleChange(uuid, packet.args) - ControlType.REMOVE -> handleRemove(uuid) - } - } - - private fun handleRemove(uuid: UUID) { - ParticleStyleManager.clientViewStyles[uuid]?.remove() - } - - /** 处理客户端同步修改 */ - private fun handleChange( - uuid: UUID, - args: Map> - ) { - // 确保他在 clientView - val style = ParticleStyleManager.clientViewStyles[uuid] ?: return - if (args.containsKey("pos")) { - val pos = args["pos"]!!.loadedValue as Vec3 - style.teleportTo(pos) - } - if (args.containsKey("teleport")) { - val pos = args["teleport"]!!.loadedValue as Vec3 - style.teleportTo(pos) - } - if (args.containsKey("rotate_to") && args.containsKey("rotate_angle")) { - style.rotateToWithAngle( - args["rotate_to"]!!.loadedValue as RelativeLocation, - args["rotate_angle"]!!.loadedValue as Double - ) - } else { - if (args.containsKey("rotate_angle")) { - style.rotateAsAxis(args["rotate_angle"]!!.loadedValue as Double) - } - if (args.containsKey("rotate_to")) { - style.rotateToPoint(args["rotate_to"]!!.loadedValue as RelativeLocation) - } - } - if (args.containsKey("axis")) { - style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) - } - if (args.containsKey("scale")) { - style.scale = args["scale"]!!.loadedValue as Double - } - if (args.containsKey("lastUpdatedGameTime")) { - style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long - } - if (args.containsKey("displayedTime")) { - style.displayedTime = args["displayedTime"]!!.loadedValue as Long - } - style.readPacketArgs(args) - } - - /** 处理客户端同步创建 */ - private fun handleCreate( - uuid: UUID, - args: Map> - ) { - val clazz = args["style_type"]!!.loadedValue as String - val builderType = Class.forName(clazz) - val builder = ParticleStyleManager.getBuilder(builderType as Class) ?: return - val style = builder.createStyle(uuid, args) - val pos = args["pos"]!!.loadedValue as Vec3 - style.rotate = args["rotate"]!!.loadedValue as Double - style.axis = RelativeLocation.of(args["axis"]!!.loadedValue as Vec3) - style.scale = args["scale"]!!.loadedValue as Double - style.lastUpdatedGameTime = args["lastUpdatedGameTime"]!!.loadedValue as Long - style.displayedTime = args["displayedTime"]!!.loadedValue as Long - val world = Minecraft.getInstance().level - style.readPacketArgs(args) - ParticleStyleManager.spawnStyle(world!!, pos, style) - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt deleted file mode 100644 index b323f3f5..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRenderEntityPacketHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.coofx.server.CooFxSceneRenderEntity -import cn.coostack.cooparticlesapi.network.packet.server.PacketRenderEntityS2C -import cn.coostack.cooparticlesapi.coofx.client.CooFxSceneClientRegistry -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.renderer.client.ClientRenderEntityManager -import cn.coostack.cooparticlesapi.renderer.runtime.ClientRenderEntityRegistry -import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityInstance -import cn.coostack.cooparticlesapi.renderer.runtime.RenderEntityRenderer -import cn.coostack.cooparticlesapi.renderer.RenderEntity -import io.netty.buffer.Unpooled -import net.minecraft.network.PacketByteBuf -import net.minecraft.resources.ResourceLocation - -/** 把服务端 RenderEntity 同步包应用到客户端实体运行时。 */ -object ClientRenderEntityPacketHandler { - /** - * 解码同步数据,并在客户端线程执行创建、更新或移除操作。 - * - * @param packet 服务端发送的 RenderEntity 操作和序列化数据 - * @param context 提供客户端线程与当前客户端实例的网络上下文 - */ - fun receive( - packet: PacketRenderEntityS2C, - context: ClientContext - ) { - val method = packet.method - val data = packet.entityData - val id = packet.id - val buf = PacketByteBuf(Unpooled.wrappedBuffer(data)) - val type = ClientRenderEntityRegistry.get(id) - if (type == null) { - CooParticlesConstants.logger.error( - "[CooFX-CREATE] 客户端 RenderEntity 类型未注册:method=$method, packetUuid=${packet.uuid}, typeId=$id", - ) - return - } - val entity = type.codec.decode(buf) - context.client().execute { - entity.world = context.client().level - when (method) { - PacketRenderEntityS2C.Method.CREATE -> { - val renderer = resolveRenderer(entity, id) - val instance = RenderEntityInstance(entity, renderer) - ClientRenderEntityManager.add(instance) - (entity as? CooFxSceneRenderEntity)?.let { created -> - CooFxSceneClientRegistry.onCreated(created) - } - } - - PacketRenderEntityS2C.Method.TOGGLE -> { - val instance = ClientRenderEntityManager.getFrom(packet.uuid) - instance?.updateFrom(entity) - (instance?.entity as? CooFxSceneRenderEntity)?.let { updated -> - CooFxSceneClientRegistry.onUpdated(updated) - } - } - - PacketRenderEntityS2C.Method.REMOVE -> { - ClientRenderEntityManager.getFrom(packet.uuid)?.markRemoved() - CooFxSceneClientRegistry.onRemoved(packet.uuid) - } - } - } - } - - /** - * 解析实体使用的 renderer。旧式实体自带 renderer 时保留实例行为,V2 类型从注册表取共享实例。 - * - * @param entity 已解码的客户端实体 - * @param id 实体类型注册 id - * @return 可渲染当前实体的 renderer - * @throws IllegalStateException 类型没有注册 renderer 时抛出 - */ - @Suppress("UNCHECKED_CAST") - private fun resolveRenderer( - entity: RenderEntity, - id: ResourceLocation - ): RenderEntityRenderer { - if (entity is RenderEntityRenderer<*>) { - return entity as RenderEntityRenderer - } - val renderer = ClientRenderEntityRegistry.resolveRenderer(id) - ?: throw IllegalStateException("RenderEntity renderer not registered: $id") - return renderer as RenderEntityRenderer - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt deleted file mode 100644 index b447f9e3..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientRendererPostEffectHandler.kt +++ /dev/null @@ -1,41 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.packet.server.PacketRendererPostEffectS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.renderer.post.CooPostEffects -import cn.coostack.cooparticlesapi.renderer.post.PostEffectRuntimeRegistry - -object ClientRendererPostEffectHandler { - fun receive(packet: PacketRendererPostEffectS2C, context: ClientContext) { - context.client().execute { - apply(packet) - } - } - - internal fun apply(packet: PacketRendererPostEffectS2C) { - when (packet.operation) { - PacketRendererPostEffectS2C.Operation.CREATE, - PacketRendererPostEffectS2C.Operation.UPDATE -> applyState(packet) - PacketRendererPostEffectS2C.Operation.REMOVE -> CooPostEffects.client.remove(packet.instanceId) - } - } - - private fun applyState(packet: PacketRendererPostEffectS2C) { - val state = packet.state ?: return - if (!PostEffectRuntimeRegistry.containsType(state.effectType)) { - CooParticlesConstants.logger.warn( - "Skipping synced post effect id={} because type={} is not registered on the client", - state.instanceId, - state.effectType - ) - return - } - val instance = state.instantiate(serverSynced = true) ?: return - when (packet.operation) { - PacketRendererPostEffectS2C.Operation.CREATE -> CooPostEffects.client.add(instance) - PacketRendererPostEffectS2C.Operation.UPDATE -> CooPostEffects.client.update(instance) - PacketRendererPostEffectS2C.Operation.REMOVE -> Unit - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt deleted file mode 100644 index 34d09f43..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundInstanceHandler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundInstanceS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.supports.sound.ClientSoundManager - -object ClientSoundInstanceHandler { - fun receive(payload: PacketSoundInstanceS2C, context: ClientContext) { - ClientSoundManager.handle(payload) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt deleted file mode 100644 index 613486cc..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/client/listener/ClientSoundLoopHandler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.client.listener - -import cn.coostack.cooparticlesapi.network.packet.server.PacketSoundLoopS2C -import cn.coostack.cooparticlesapi.platform.network.ClientContext -import cn.coostack.cooparticlesapi.supports.sound.ClientSoundLoopManager - -object ClientSoundLoopHandler { - fun receive(payload: PacketSoundLoopS2C, context: ClientContext) { - ClientSoundLoopManager.handle(payload) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt deleted file mode 100644 index 32a3b9b2..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketBindStarfieldFboBlockS2C.kt +++ /dev/null @@ -1,67 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.renderer.pipeline.CooBlockPipelines -import cn.coostack.cooparticlesapi.test.options.renderer.pipeline.RenderPipelineExamples -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.level.block.Blocks -import net.minecraft.world.level.block.state.BlockState -import java.util.UUID - -/** - * 把方块测试选中的方块类型同步为客户端星空 FBO 临时绑定。 - * - * BlockState 只用于通过注册表同步方块类型。每次测试都有独立的 [bindingId],结束时只撤销 - * 自己的规则,之前的永久绑定或其他测试绑定会继续生效。 - */ -@CooAutoRegister -class PacketBindStarfieldFboBlockS2C() : CooPacket() { - /** 用于同步目标方块类型的状态载体。 */ - @CodecField - var state: BlockState = Blocks.AIR.defaultBlockState() - - /** 当前临时绑定的所有者 ID。 */ - @CodecField - var bindingId: UUID = UUID(0L, 0L) - - /** `true` 添加临时绑定,`false` 撤销该所有者的绑定。 */ - @CodecField - var enabled: Boolean = true - - /** - * @param state 测试位置的方块状态,用于传递方块类型 - * @param bindingId 当前测试的临时绑定 ID - * @param enabled 是否启用星空 FBO 绑定 - */ - constructor(state: BlockState, bindingId: UUID, enabled: Boolean) : this() { - this.state = state - this.bindingId = bindingId - this.enabled = enabled - } - - /** @return 星空 FBO 方块类型绑定包的稳定资源 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** - * @param context 当前客户端网络上下文 - */ - override fun onClientReceive(context: ClientContext) { - context.client.execute { - if (enabled) { - CooBlockPipelines.bindScoped(bindingId, state.block, RenderPipelineExamples.STARFIELD_BLOCK) - } else { - CooBlockPipelines.unbindScoped(bindingId) - } - } - } - - companion object { - private const val PACKET_ID = "bind_starfield_fbo_block_s2c" - } -} 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 index 1aa48a6b..5e68d28f 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -18,7 +17,7 @@ class PacketCameraShakeS2C( val yawOffset: Float, val pitchOffset: Float, val instant: Boolean -) { +) { constructor(range: Double, origin: Vec3, amplitude: Double, tick: Int) : this( CameraOperation.SHAKE, range, @@ -44,45 +43,46 @@ class PacketCameraShakeS2C( companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "camara_shake") - val payloadID = ResourceLocation(identifierID) + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "camara_shake") - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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 - ) - }) + 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) @@ -197,6 +197,7 @@ class PacketCameraShakeS2C( } private fun operationFromId(id: Int): CameraOperation { - return CameraOperation.values().firstOrNull { it.id == id } ?: CameraOperation.SHAKE + return CameraOperation.entries.firstOrNull { it.id == id } ?: CameraOperation.SHAKE } - } \ No newline at end of file + } +} 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 index 399e58d3..c04c04fe 100644 --- 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 @@ -2,13 +2,12 @@ package cn.coostack.cooparticlesapi.network.packet.server import cn.coostack.cooparticlesapi.CooParticlesConstants import net.minecraft.network.PacketByteBuf - import net.minecraft.resources.ResourceLocation -object PacketClearClientStateS2C { +object PacketClearClientStateS2C { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "clear_client_state") - val payloadID = ResourceLocation(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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/PacketCooFxModelBlockTestS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt deleted file mode 100644 index db295522..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/PacketCooFxModelBlockTestS2C.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server - -/** - * 旧版 BlockTest 的一次性 CooFX S2C 包已由服务端权威 `CooFxSceneManager` 和 RenderEntity - * CREATE/TOGGLE/REMOVE 协议替代。本文件保留迁移说明,避免再次引入测试专用网络状态。 - */ 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 index 41d9b136..d919c2c0 100644 --- 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 @@ -1,21 +1,15 @@ package cn.coostack.cooparticlesapi.network.packet.server import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.data.holder.DataHolder -import cn.coostack.cooparticlesapi.data.holder.DataHolderKey -import cn.coostack.cooparticlesapi.data.holder.DataHolderManager -import io.netty.buffer.Unpooled -import net.minecraft.client.Minecraft import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation -import net.minecraft.world.entity.Entity class PacketDataHolderS2C( val entityId: Int, val cacheAllToggle: Boolean, val fullSync: Boolean, val entries: List -) { +) { data class Entry( val key: String, val type: String, @@ -24,78 +18,72 @@ class PacketDataHolderS2C( companion object { private val id = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "data_holder") - val payloadID = ResourceLocation(id) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.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) - }) + 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: Entity, holder: DataHolder, fullSync: Boolean): PacketDataHolderS2C { + 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: Entity): ByteArray { - val codec = DataHolderManager.getCodecFromID(type) + 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( - Unpooled.buffer(), - entity.level().registryAccess() - ) + val buf = PacketByteBuf(io.netty.buffer.Unpooled.buffer()) @Suppress("UNCHECKED_CAST") - (codec as ).encode(buf, value) + (codec as CommonStreamCodec).encode(buf, value) val data = ByteArray(buf.readableBytes()) buf.readBytes(data) + buf.release() return data } } - fun decodeData(): Map, Any> { - val entity = Minecraft.getInstance().level?.getEntity(entityId) + 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 = DataHolderKey.ofRaw(keyType, ResourceLocation.parse(entry.key)) + 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: Entity): Any { - val codec = DataHolderManager.getCodecFromID(type) + 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( - Unpooled.wrappedBuffer(data), - Minecraft.getInstance().connection?.registryAccess() - ?: entity.level().registryAccess() - ) + val buf = PacketByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data)) @Suppress("UNCHECKED_CAST") - return (codec as cn.coostack.cooparticlesapi.network.packet.api.CommonCodec).decode(buf) + 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 index 182f6689..a7528349 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -11,21 +10,23 @@ class PacketDisplayEntityS2C( 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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) - }) - } \ No newline at end of file + 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 index 14ad0ebc..0c261e1b 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -14,25 +13,27 @@ class PacketDisplayEntityStateS2C( 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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(), - ) - }) + 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 index d2df37fa..bfaac96f 100644 --- 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 @@ -2,17 +2,18 @@ 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) { +class PacketKeyBindingCountdownS2C(val key: ResourceLocation, val cd: Int) { companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "key_binding_countdown") - val payloadID = ResourceLocation(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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()) - }) - } \ No newline at end of file + }, { 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 index 61b94026..8826c749 100644 --- 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 @@ -4,7 +4,6 @@ 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 @@ -12,7 +11,7 @@ class PacketParticleBatchS2C( val type: ParticleOptions, val positions: List, val velocity: Vec3, -) { +) { init { require(positions.size <= MAX_PARTICLES) { "particle batch exceeds $MAX_PARTICLES entries" } } @@ -21,17 +20,19 @@ class PacketParticleBatchS2C( const val MAX_PARTICLES = 4096 private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_batch") - val payloadID = ResourceLocation(identifierID) - val CODEC: = + 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) - }) + 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 index 79a52a88..94c1f892 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -11,19 +10,21 @@ 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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) - }) - } \ No newline at end of file + 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 index d71ff8e8..9bfa6c3e 100644 --- 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 @@ -2,38 +2,35 @@ 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) { - - /** - * 是否是因为距离过长而移除 - */ +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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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 - } - }) - } \ No newline at end of file + 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 index 66108d6f..2680be04 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -15,27 +14,29 @@ class PacketParticleCompositionStateS2C( 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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(), - ) - }) + 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 index df1458ef..b71f7144 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -11,15 +10,7 @@ class PacketParticleEmittersS2C( val emitterUUID: UUID, val emitterData: ByteArray, val type: PacketType -) { - /** - * 发射器同步包的生命周期操作。 - * - * [CREATE] 用于首次进入玩家可见范围,客户端必须创建并启动新实例; - * [CHANGE] 只更新已经存在的客户端实例,不得重新执行启动逻辑; - * [REMOVE] 按 UUID 立即清理客户端实例,不携带完整发射器状态。 - * 数字 ID 是网络协议的一部分,其中 `0` 和 `1` 保留旧协议的创建与移除编号。 - */ +) { enum class PacketType(val id: Int) { CREATE(0), REMOVE(1), @@ -41,27 +32,26 @@ class PacketParticleEmittersS2C( companion object { private val id = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_emitters") - val payloadID = ResourceLocation(id) + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_emitters") - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.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) - ) - }) + 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 index f601a7de..2068e292 100644 --- 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 @@ -1,41 +1,15 @@ package cn.coostack.cooparticlesapi.network.packet.server import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.particles.control.ControlType import net.minecraft.network.PacketByteBuf - import net.minecraft.resources.ResourceLocation import java.util.UUID -/** - * 三个参数 - * @param uuid 更改的粒子组的UUID - * @param type 三种类型对应三种不同的包参数 - * 1. REMOVE args什么都不写 - * 2. CREATE args输入 - * 1. pos: Vec3d - * 2. groupType: Class - * 3. currentTick: Int - * 4. maxTick: Int - * 5. ByteBuf 用于ControlableParticleGroup 构建的输入参数(不包括自身的UUID) - * 6. scale 缩放大小 - * 3. CHANGE args输入支持 - * 1. pos: Vec3d - * 2. rotateTo: Vec3d - * 3. rotateAxis: Double / Float - * 4. axis: Vec3d - * 5. currentTick: Int - * 6. maxTick: Int - * 7. invoke: 无参方法名 - * 8. scale 缩放大小 - */ class PacketParticleGroupS2C( val uuid: UUID, - val type: ControlType, - val args: Map> -) { + val type: cn.coostack.cooparticlesapi.particles.control.ControlType, + val args: Map> +) { enum class PacketArgsType(val ofArgs: String) { POS("pos"), CURRENT_TICK("current_tick"), @@ -50,14 +24,14 @@ class PacketParticleGroupS2C( companion object { fun fromArgsName(value: String): PacketArgsType { return when (value) { - "pos" -> return POS - "current_tick" -> return CURRENT_TICK - "max_tick" -> return MAX_TICK - "rotate_to" -> return ROTATE_TO - "rotate_axis" -> return ROTATE_AXIS - "invoke" -> return INVOKE - "scale" -> return SCALE - "groupType" -> return GROUP_TYPE + "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 } } @@ -66,35 +40,33 @@ class PacketParticleGroupS2C( companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "particle_group") - val payloadID = ResourceLocation(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, packet -> - buf.writeUUID(packet.uuid) - buf.writeInt(packet.type.id) - packet.args.forEach { (t, u) -> - val encode = 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 = 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 = ParticleControlerDataBuffers.decodeToBuffer(value) - args[key] = decode - } - PacketParticleGroupS2C( - uuid, type, args - ) - }) - + 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 + ) + }) } -} \ No newline at end of file +} 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 index 1803fb75..adc24de5 100644 --- 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 @@ -1,35 +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.core.particles.ParticleTypes 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, packet -> - buf.writeVec3(packet.pos) - buf.writeVec3(packet.velocity) - ParticleTypes.STREAM_CODEC.encode(buf, packet.type) - }, { buf -> - val pos = buf.readVec3() - val velocity = buf.readVec3() - val type = ParticleTypes.STREAM_CODEC.decode(buf) - PacketParticleS2C(type, pos, velocity) - }) + 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) + }) } -} \ No newline at end of file +} 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 index e065d66d..61ab8cf5 100644 --- 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 @@ -1,49 +1,45 @@ package cn.coostack.cooparticlesapi.network.packet.server import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.particles.control.ControlType import net.minecraft.network.PacketByteBuf - import net.minecraft.resources.ResourceLocation import java.util.UUID class PacketParticleStyleS2C( val uuid: UUID, - val type: ControlType, - val args: Map> -) { + 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(identifierID) - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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 = 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 = ControlType.getTypeById(id) - val argsCount = buf.readInt() - repeat(argsCount) { - val len = buf.readInt() - val key = buf.readUtf() - val buf = buf.readBytes(len) - val decode = ParticleControlerDataBuffers.decodeToBuffer(buf) - args[key] = decode - } - return PacketParticleStyleS2C( - uuid, type, args - ) + 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 ) - - } \ No newline at end of file + }) + } +} 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 index ecaa0468..10e45036 100644 --- 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 @@ -2,11 +2,10 @@ 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) { +class PacketRenderEntityS2C(var uuid: UUID, var entityData: ByteArray, var id: ResourceLocation, var method: Method) { enum class Method(val id: Int) { CREATE(0), TOGGLE(1), @@ -27,24 +26,24 @@ class PacketRenderEntityS2C(var uuid: UUID, var entityData: ByteArray, var id: R companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_entity_packet") - val payloadID = ResourceLocation(identifierID) + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_entity_packet") - @JvmStatic - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = + 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) - val packet = PacketRenderEntityS2C(uuid, bytes, id, Method.idOf(method)) - return packet - } - ) - } \ No newline at end of file + 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 index 45e6d8ac..6494b121 100644 --- 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 @@ -5,15 +5,15 @@ 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), @@ -29,20 +29,19 @@ class PacketRendererPostEffectS2C private constructor( companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_post_effect_packet") - val payloadID = ResourceLocation(identifierID) + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "renderer_post_effect_packet") - @JvmStatic - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, packet -> - 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) - }) + 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) 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 index 05cb86d4..e9ed2546 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -24,7 +23,7 @@ class PacketSoundInstanceS2C( val whitelistSounds: Set, val whitelistSources: Set, val whitelistKeys: Set -) { +) { enum class Action { PLAY, UPDATE, @@ -36,11 +35,14 @@ class PacketSoundInstanceS2C( companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_instance") - val payloadID = ResourceLocation(identifierID) + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_instance") private val emptySound = ResourceLocation.withDefaultNamespace("empty") - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = buf.writeUtf(packet.key) + 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) @@ -231,4 +233,5 @@ class PacketSoundInstanceS2C( } return result } - } \ No newline at end of file + } +} 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 index 85f234b9..6e95568e 100644 --- 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 @@ -2,7 +2,6 @@ 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 @@ -17,12 +16,15 @@ class PacketSoundLoopS2C( val pitch: Float, val start: Boolean, val stopImmediately: Boolean -) { +) { companion object { private val identifierID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_loop") - val payloadID = ResourceLocation(identifierID) + val payloadID = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "sound_loop") - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = buf.writeResourceLocation(packet.sound) + 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) @@ -71,4 +73,5 @@ class PacketSoundLoopS2C( stopImmediately = interrupt ) } - } \ No newline at end of file + } +} 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 index fb0d5831..4eb13b50 100644 --- 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 @@ -1,83 +1,36 @@ package cn.coostack.cooparticlesapi.network.packet.server import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket 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 -/** - * 从服务端向客户端同步一个地形效果组的替换或增量更新。 - * - * 协议支持完整替换、追加位置、移除位置、替换 uniform 和删除整组。位置使用相对首坐标的 ZigZag - * 差值编码,生效时间使用相对首个绝对 tick 的差值编码,避免大批连续方块重复传输完整坐标和时间。 - * 包实例只由 companion object 中的工厂创建,并由 - * [cn.coostack.cooparticlesapi.renderer.terrain.CooTerrainEffectManager] 发送。 - */ -@CooAutoRegister -class PacketTerrainEffectGroupS2C() : CooPacket() { - /** 当前包的协议操作类型。 */ +class PacketTerrainEffectGroupS2C { var operation: Int = REPLACE - - /** 目标效果组所属的维度 ID。 */ var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") - - /** 目标效果组 ID。 */ var groupId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") - - /** 完整替换操作引用的方块 Pipeline ID。 */ var pipelineId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") - - /** 完整替换操作中组开始的绝对游戏 tick。 */ var startedAt: Long = 0L - - /** 完整替换操作中组到期的绝对游戏 tick;`null` 表示持久组。 */ var expiresAt: Long? = null - - /** 完整替换操作中用于重叠组排序的服务端序号。 */ var sequence: Long = 0L - - /** 用于丢弃重复或乱序更新的单调协议版本号。 */ var revision: Long = 0L - - /** 完整替换和 options 更新中的有符号绘制优先级。 */ var priority: Int = 0 - - /** 完整替换和 options 更新中的合成方式。 */ var composition: CooTerrainEffectComposition = CooTerrainEffectComposition.REPLACE - - /** - * 操作涉及的位置数据。 - * - * 完整替换和追加操作中,键为方块坐标,值为绝对生效 tick;移除位置操作只使用键。 - */ var positions: Map = emptyMap() - - /** 键为 shader uniform 名称,值为完整替换或 uniform 更新携带的数据。 */ var uniforms: Map = emptyMap() - /** @return 地形效果组服务端到客户端协议的固定资源 ID。 */ override fun id(): ResourceLocation = ResourceLocation.fromNamespaceAndPath( CooParticlesConstants.MOD_ID, PACKET_ID ) - /** @return 编解码本协议全部操作类型的 CommonCodec。 */ - override fun codec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = CODEC + override fun codec(): CommonStreamCodec = CODEC - /** - * 在客户端主线程把本包更新应用到 [CooTerrainEffectRegistry]。 - * - * @param context 当前客户端网络处理上下文 - */ - override fun onClientReceive(context: ClientContext) { + override fun onClientReceive(context: cn.coostack.cooparticlesapi.network.packet.api.ClientContext) { context.client.execute { when (operation) { REPLACE -> CooTerrainEffectRegistry.install( @@ -120,7 +73,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** 创建各协议操作并实现紧凑的坐标、时间和 uniform 编解码。 */ companion object { private const val PACKET_ID = "terrain_effect_group_s2c" private const val REPLACE = 0 @@ -130,15 +82,8 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { private const val REMOVE_GROUP = 4 private const val UPDATE_GROUP_OPTIONS = 5 - private val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of(::encode, ::decode) + val CODEC: CommonStreamCodec = CommonStreamCodec.of(::encode, ::decode) - /** - * 创建完整替换效果组的协议包。 - * - * @param snapshot 要同步的完整服务端效果组快照 - * @return 携带 Pipeline、时间、位置和 uniform 的替换包 - */ internal fun replace(snapshot: CooTerrainEffectGroupSnapshot): PacketTerrainEffectGroupS2C { return PacketTerrainEffectGroupS2C().also { it.operation = REPLACE @@ -173,14 +118,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 创建只追加位置和绝对生效时间的协议包。 - * - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @param positions 键为新增方块坐标,值为绝对生效 tick - * @return 位置追加包 - */ internal fun append( dimension: ResourceLocation, groupId: ResourceLocation, @@ -196,14 +133,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 创建删除整个效果组的协议包。 - * - * @param dimension 组所属维度 ID - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @return 整组删除包 - */ internal fun remove( dimension: ResourceLocation, groupId: ResourceLocation, @@ -217,15 +146,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 创建只删除组内指定位置的协议包。 - * - * @param dimension 组所属维度 ID - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @param positions 要删除的方块坐标 - * @return 位置删除包 - */ internal fun removePositions( dimension: ResourceLocation, groupId: ResourceLocation, @@ -241,15 +161,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 创建替换整组 uniform 集合的协议包。 - * - * @param dimension 组所属维度 ID - * @param groupId 组 ID - * @param revision 本次更新的单调协议版本号 - * @param uniforms 键为 shader uniform 名称,值为新的完整 uniform 数据 - * @return uniform 更新包 - */ internal fun updateUniforms( dimension: ResourceLocation, groupId: ResourceLocation, @@ -265,7 +176,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** 按操作类型写入公共头和对应载荷。 */ private fun encode(buffer: PacketByteBuf, packet: PacketTerrainEffectGroupS2C) { buffer.writeByte(packet.operation) buffer.writeResourceLocation(packet.dimension) @@ -293,7 +203,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** 从公共头中的操作类型选择对应载荷读取流程。 */ private fun decode(buffer: PacketByteBuf): PacketTerrainEffectGroupS2C { val packet = PacketTerrainEffectGroupS2C() packet.operation = buffer.readUnsignedByte().toInt() @@ -322,12 +231,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { return packet } - /** - * 以首项为坐标和时间基准写入一组位置。 - * - * @param buffer 目标网络缓冲 - * @param positions 键为方块坐标,值为该位置的绝对生效 tick - */ private fun writeTimedPositions(buffer: PacketByteBuf, positions: Map) { val entries = positions.entries.toList() buffer.writeVarInt(entries.size) @@ -344,12 +247,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 读取以首项为基准编码的位置和绝对生效时间。 - * - * @param buffer 源网络缓冲 - * @return 键为方块坐标、值为绝对生效 tick 的映射 - */ private fun readTimedPositions(buffer: PacketByteBuf): Map { val count = buffer.readVarInt() if (count == 0) return emptyMap() @@ -367,7 +264,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { return result } - /** 以首个方块坐标为基准写入无需附带时间的位置集合。 */ private fun writePositions(buffer: PacketByteBuf, positions: Collection) { val entries = positions.toList() buffer.writeVarInt(entries.size) @@ -381,7 +277,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** @return 从相对坐标差值还原出的方块位置集合。 */ private fun readPositions(buffer: PacketByteBuf): Set { val count = buffer.readVarInt() if (count == 0) return emptySet() @@ -399,12 +294,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 写入带类型标记的 uniform 映射。 - * - * @param buffer 目标网络缓冲 - * @param uniforms 键为 uniform 名称,值为浮点数、整数或向量数据 - */ private fun writeUniforms(buffer: PacketByteBuf, uniforms: Map) { buffer.writeVarInt(uniforms.size) uniforms.forEach { (name, value) -> @@ -413,12 +302,6 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } } - /** - * 读取带类型标记的 uniform 映射。 - * - * @param buffer 源网络缓冲 - * @return 键为 uniform 名称、值为对应类型数据的映射 - */ private fun readUniforms(buffer: PacketByteBuf): Map { val count = buffer.readVarInt() val result = LinkedHashMap(count) @@ -430,11 +313,8 @@ class PacketTerrainEffectGroupS2C() : CooPacket() { } 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 index 7c95aa59..5d24e2d2 100644 --- 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 @@ -1,25 +1,15 @@ package cn.coostack.cooparticlesapi.network.packet.server import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket 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 -/** - * 程序化 terrain mapping 的服务端到客户端同步包。 - * - * 包只携带区域参数和实例参数,不携带 BlockPos、非空气方块或高度图结果。 - */ -@CooAutoRegister -class PacketTerrainMappingS2C() : CooPacket() { +class PacketTerrainMappingS2C { var operation: Int = REPLACE var dimension: ResourceLocation = ResourceLocation.withDefaultNamespace("overworld") var instanceId: ResourceLocation = ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "empty") @@ -40,9 +30,9 @@ class PacketTerrainMappingS2C() : CooPacket() { PACKET_ID ) - override fun codec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = CODEC + override fun codec(): CommonStreamCodec = CODEC - override fun onClientReceive(context: ClientContext) { + override fun onClientReceive(context: cn.coostack.cooparticlesapi.network.packet.api.ClientContext) { context.client.execute { when (operation) { REPLACE -> CooTerrainMappingRegistry.install(toInstance()) @@ -77,8 +67,7 @@ class PacketTerrainMappingS2C() : CooPacket() { private const val REPLACE = 0 private const val UPDATE_UNIFORMS = 1 private const val REMOVE = 2 - private val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of(::encode, ::decode) + val CODEC: CommonStreamCodec = CommonStreamCodec.of(::encode, ::decode) internal fun replace(instance: CooTerrainMappingInstance, pipelineId: ResourceLocation): PacketTerrainMappingS2C = PacketTerrainMappingS2C().also { diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt deleted file mode 100644 index d010466c..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/server/listener/ServerKeyActionHandler.kt +++ /dev/null @@ -1,17 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.server.listener - -import cn.coostack.cooparticlesapi.event.CooEventBus -import cn.coostack.cooparticlesapi.event.events.key.KeyActionEvent -import cn.coostack.cooparticlesapi.network.packet.client.PacketKeyActionC2S -import cn.coostack.cooparticlesapi.platform.network.ServerContext - -object ServerKeyActionHandler { - fun receive(packet: PacketKeyActionC2S, context: ServerContext) { - val player = context.player() - context.server().execute { - CooEventBus.call( - KeyActionEvent(player, packet.keyActions, true) - ) - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt deleted file mode 100644 index bc7b67d0..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusControlS2C.kt +++ /dev/null @@ -1,38 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.status - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.performance.PerformanceStatusClientBridge -import cn.coostack.cooparticlesapi.performance.PerformanceStatusControlAction -import net.minecraft.resources.ResourceLocation - -/** 服务端命令向目标玩家客户端发送的 Status 会话控制包。 */ -@CooAutoRegister -class PacketPerformanceStatusControlS2C() : CooPacket() { - /** 网络动作名称。 */ - @CodecField var action: String = "" - - /** 使用类型安全动作创建控制包。 */ - constructor(action: PerformanceStatusControlAction) : this() { - this.action = action.name - } - - /** 返回该业务包的稳定协议 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** 经纯 common bridge 把动作交给客户端初始化时安装的控制器。 */ - override fun onClientReceive(context: ClientContext) { - PerformanceStatusControlAction.fromNetworkName(action)?.let(PerformanceStatusClientBridge::handle) - } - - /** 协议注册常量。 */ - companion object { - /** 业务包稳定注册 ID,属于网络协议的一部分。 */ - private const val PACKET_ID = "performance_status_control_s2c" - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt deleted file mode 100644 index feb102c0..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusRequestC2S.kt +++ /dev/null @@ -1,39 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.status - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.CooPacketKind -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerRequestGate -import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshotFactory -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.resources.ResourceLocation - -/** 客户端在 Status 活跃期间按配置间隔发送的无参数服务端快照请求。 */ -@CooAutoRegister -class PacketPerformanceStatusRequestC2S : CooPacket() { - /** 返回该业务包的稳定协议 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** 验证请求类型、命令权限和服务端限流后构造固定大小快照。 */ - override fun onServerReceive(context: ServerContext) { - if (context.kind != CooPacketKind.REQUEST) return - val refreshInterval = CooParticlesServices.API_CONFIG_MANAGER.getConfig() - .statusServerRefreshIntervalTicks - if (!PerformanceStatusServerRequestGate.tryAcquire(context.sender, refreshInterval)) return - context.reply( - PacketPerformanceStatusSnapshotS2C.from( - PerformanceStatusServerSnapshotFactory.create(context.sender.server) - ) - ) - } - - /** 协议注册常量。 */ - companion object { - /** 业务包稳定注册 ID,属于网络协议的一部分。 */ - private const val PACKET_ID = "performance_status_request_c2s" - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt deleted file mode 100644 index 7094ae10..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/status/PacketPerformanceStatusSnapshotS2C.kt +++ /dev/null @@ -1,191 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.status - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.performance.PerformanceStatusNetworkTotals -import cn.coostack.cooparticlesapi.performance.PerformanceStatusServerSnapshot -import cn.coostack.cooparticlesapi.performance.PerformanceStatusVanillaPacketTotals -import net.minecraft.resources.ResourceLocation - -/** 服务端返回给单个查看者的固定字段 Status 快照。 */ -@CooAutoRegister -class PacketPerformanceStatusSnapshotS2C : CooPacket() { - /** 服务端平均 MSPT。 */ - @CodecField var averageMspt: Double = 0.0 - - /** 服务端 Barrage 数。 */ - @CodecField var barrages: Int = 0 - - /** 服务端生成快照的 epoch 毫秒。 */ - @CodecField var capturedAtEpochMillis: Long = 0L - - /** 服务端 CooFX scene 数。 */ - @CodecField var cooFxScenes: Int = 0 - - /** 服务端 Composition 数。 */ - @CodecField var compositions: Int = 0 - - /** 服务端 DisplayEntity 数。 */ - @CodecField var displayEntities: Int = 0 - - /** 服务端 Emitter 数。 */ - @CodecField var emitters: Int = 0 - - /** 服务端 JVM 累计垃圾收集次数。 */ - @CodecField var gcCollectionCount: Long = 0L - - /** 服务端 JVM 累计垃圾收集耗时毫秒。 */ - @CodecField var gcCollectionTimeMs: Long = 0L - - /** 服务端最大堆字节数。 */ - @CodecField var heapMaxBytes: Long = 0L - - /** 服务端已使用堆字节数。 */ - @CodecField var heapUsedBytes: Long = 0L - - /** 服务端历史最大 MSPT。 */ - @CodecField var maxMspt: Double = 0.0 - - /** 服务端累计收到的 CooPacket 业务字节数。 */ - @CodecField var networkReceivedBytes: Long = 0L - - /** 服务端累计收到的 CooPacket 数。 */ - @CodecField var networkReceivedPackets: Long = 0L - - /** 服务端累计发送的 CooPacket 业务字节数。 */ - @CodecField var networkSentBytes: Long = 0L - - /** 服务端累计发送的 CooPacket 数。 */ - @CodecField var networkSentPackets: Long = 0L - - /** 当前在线玩家数。 */ - @CodecField var onlinePlayers: Int = 0 - - /** 服务端历史 P95 MSPT。 */ - @CodecField var p95Mspt: Double = 0.0 - - /** 服务端旧 ParticleGroup 数。 */ - @CodecField var particleGroups: Int = 0 - - /** 服务端配置的 Status 快照刷新间隔。 */ - @CodecField var refreshIntervalTicks: Int = 20 - - /** 服务端 RenderEntity 数。 */ - @CodecField var renderEntities: Int = 0 - - /** 服务端当前 tick 序号。 */ - @CodecField var serverTick: Long = 0L - - /** 服务端管理的声音实例数。 */ - @CodecField var soundInstances: Int = 0 - - /** 服务端管理的循环声音数。 */ - @CodecField var soundLoops: Int = 0 - - /** 当前目标 TPS。 */ - @CodecField var targetTps: Double = 20.0 - - /** 服务端 Terrain effect group 数。 */ - @CodecField var terrainEffectGroups: Int = 0 - - /** 服务端 Terrain mapping 实例数。 */ - @CodecField var terrainMappings: Int = 0 - - /** 当前可维持 TPS。 */ - @CodecField var tps: Double = 20.0 - - /** 服务端原版 Connection 累计收到的 Packet 数。 */ - @CodecField var vanillaNetworkReceivedPackets: Long = 0L - - /** 服务端原版 Connection 累计发送的 Packet 数。 */ - @CodecField var vanillaNetworkSentPackets: Long = 0L - - /** 返回该业务包的稳定协议 ID。 */ - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, PACKET_ID) - } - - /** 把协议字段恢复为不可变服务端快照。 */ - fun toSnapshot(): PerformanceStatusServerSnapshot { - return PerformanceStatusServerSnapshot( - capturedAtEpochMillis = capturedAtEpochMillis, - serverTick = serverTick, - refreshIntervalTicks = refreshIntervalTicks.coerceIn(5, 1_200), - targetTps = targetTps, - tps = tps, - averageMspt = averageMspt, - p95Mspt = p95Mspt, - maxMspt = maxMspt, - onlinePlayers = onlinePlayers, - particleGroups = particleGroups, - renderEntities = renderEntities, - displayEntities = displayEntities, - emitters = emitters, - compositions = compositions, - gcCollectionCount = gcCollectionCount, - gcCollectionTimeMs = gcCollectionTimeMs, - terrainEffectGroups = terrainEffectGroups, - terrainMappings = terrainMappings, - soundInstances = soundInstances, - soundLoops = soundLoops, - barrages = barrages, - cooFxScenes = cooFxScenes, - heapUsedBytes = heapUsedBytes, - heapMaxBytes = heapMaxBytes, - cooPackets = PerformanceStatusNetworkTotals( - sentPackets = networkSentPackets, - sentBytes = networkSentBytes, - receivedPackets = networkReceivedPackets, - receivedBytes = networkReceivedBytes, - ), - vanillaPackets = PerformanceStatusVanillaPacketTotals( - sentPackets = vanillaNetworkSentPackets, - receivedPackets = vanillaNetworkReceivedPackets, - ), - ) - } - - /** 协议注册常量和快照转换入口。 */ - companion object { - /** 业务包稳定注册 ID,属于网络协议的一部分。 */ - private const val PACKET_ID = "performance_status_snapshot_s2c" - - /** 从不可变服务端快照创建可反射编码的业务包。 */ - fun from(snapshot: PerformanceStatusServerSnapshot): PacketPerformanceStatusSnapshotS2C { - return PacketPerformanceStatusSnapshotS2C().also { packet -> - packet.averageMspt = snapshot.averageMspt - packet.barrages = snapshot.barrages - packet.capturedAtEpochMillis = snapshot.capturedAtEpochMillis - packet.cooFxScenes = snapshot.cooFxScenes - packet.compositions = snapshot.compositions - packet.displayEntities = snapshot.displayEntities - packet.emitters = snapshot.emitters - packet.gcCollectionCount = snapshot.gcCollectionCount - packet.gcCollectionTimeMs = snapshot.gcCollectionTimeMs - packet.heapMaxBytes = snapshot.heapMaxBytes - packet.heapUsedBytes = snapshot.heapUsedBytes - packet.maxMspt = snapshot.maxMspt - packet.networkReceivedBytes = snapshot.cooPackets.receivedBytes - packet.networkReceivedPackets = snapshot.cooPackets.receivedPackets - packet.networkSentBytes = snapshot.cooPackets.sentBytes - packet.networkSentPackets = snapshot.cooPackets.sentPackets - packet.onlinePlayers = snapshot.onlinePlayers - packet.p95Mspt = snapshot.p95Mspt - packet.particleGroups = snapshot.particleGroups - packet.refreshIntervalTicks = snapshot.refreshIntervalTicks - packet.renderEntities = snapshot.renderEntities - packet.serverTick = snapshot.serverTick - packet.soundInstances = snapshot.soundInstances - packet.soundLoops = snapshot.soundLoops - packet.targetTps = snapshot.targetTps - packet.terrainEffectGroups = snapshot.terrainEffectGroups - packet.terrainMappings = snapshot.terrainMappings - packet.tps = snapshot.tps - packet.vanillaNetworkReceivedPackets = snapshot.vanillaPackets.receivedPackets - packet.vanillaNetworkSentPackets = snapshot.vanillaPackets.sentPackets - } - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt deleted file mode 100644 index ba8c6d2c..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenBoundTestSelectionScreenS2C.kt +++ /dev/null @@ -1,48 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.BoundControllerEntry -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketOpenBoundTestSelectionScreenS2C() : CooPacket() { - @CodecField var currentIndices: List = emptyList() - @CodecField var currentOptionIds: List = emptyList() - @CodecField var dimensions: List = emptyList() - @CodecField var groupIds: List = emptyList() - @CodecField var loaded: List = emptyList() - @CodecField var optionCounts: List = emptyList() - @CodecField var positions: List = emptyList() - @CodecField var running: List = emptyList() - @CodecField var statuses: List = emptyList() - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_bound_test_selection_screen_s2c") - } - - override fun onClientReceive(context: ClientContext) { - TestControllerClientScreens.openBoundSelection(this) - } - - companion object { - fun fromEntries(entries: List): PacketOpenBoundTestSelectionScreenS2C { - return PacketOpenBoundTestSelectionScreenS2C().also { - it.dimensions = entries.map { entry -> entry.dimension } - it.positions = entries.map { entry -> entry.pos } - it.groupIds = entries.map { entry -> entry.groupId } - it.statuses = entries.map { entry -> entry.status } - it.currentIndices = entries.map { entry -> entry.currentIndex } - it.currentOptionIds = entries.map { entry -> entry.currentOptionId } - it.optionCounts = entries.map { entry -> entry.optionCount } - it.loaded = entries.map { entry -> entry.loaded } - it.running = entries.map { entry -> entry.running } - } - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt deleted file mode 100644 index 3139e246..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketOpenTestControllerScreenS2C.kt +++ /dev/null @@ -1,123 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockEntity -import cn.coostack.cooparticlesapi.network.packet.api.ClientContext -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.test.block.client.TestControllerClientScreens -import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec -import cn.coostack.cooparticlesapi.test.api.TestOptionParamSpec -import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketOpenTestControllerScreenS2C() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var boxDepth: Double = 0.6 - @CodecField var boxHeight: Double = 1.8 - @CodecField var boxWidth: Double = 0.6 - @CodecField var currentIndex: Int = 0 - @CodecField var dimension: String = "" - @CodecField var forwardX: Double = 0.0 - @CodecField var forwardY: Double = 0.0 - @CodecField var forwardZ: Double = 1.0 - @CodecField var forwardDynamic: Boolean = false - @CodecField var forwardTrack: String = "" - @CodecField var groupId: String = "" - @CodecField var mode: String = "sequential" - @CodecField var offsetX: Double = 0.0 - @CodecField var offsetY: Double = 0.0 - @CodecField var offsetZ: Double = 0.0 - @CodecField var optionCount: Int = 0 - @CodecField var optionIds: List = emptyList() - @CodecField var optionParamSpecs: List = emptyList() - @CodecField var optionParamValues: List = emptyList() - @CodecField var pendingReview: Boolean = false - @CodecField var positionDynamic: Boolean = false - @CodecField var positionTrack: String = "" - @CodecField var registeredIds: List = emptyList() - @CodecField var registeredOptionIds: List = emptyList() - @CodecField var registeredOptionParamSpecs: List> = emptyList() - @CodecField var repeatDelayTicks: Int = 0 - @CodecField var repeatIndex: Boolean = false - @CodecField var running: Boolean = false - @CodecField var selectedIndex: Int = 0 - @CodecField var status: String = "" - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "open_test_controller_screen_s2c") - } - - override fun onClientReceive(context: ClientContext) { - TestControllerClientScreens.openController(this) - } - - companion object { - /** - * 从服务端方块实体创建控制界面快照。 - * - * @param dimension 方块所在维度的资源 ID 文本 - * @param blockEntity 待同步的测试控制器方块实体 - * @param registeredIds 当前可用的方块测试组资源 ID - * @return 可发送到客户端的完整界面数据包 - */ - fun fromBlockEntity( - dimension: String, - blockEntity: TestControllerBlockEntity, - registeredIds: List - ): PacketOpenTestControllerScreenS2C { - return PacketOpenTestControllerScreenS2C().also { - it.dimension = dimension - it.blockPos = blockEntity.blockPos - it.groupId = blockEntity.groupId - it.mode = blockEntity.mode.id - it.selectedIndex = blockEntity.selectedIndex - it.repeatIndex = blockEntity.repeatIndex - it.repeatDelayTicks = blockEntity.repeatDelayTicks - it.offsetX = blockEntity.playerOffset.x - it.offsetY = blockEntity.playerOffset.y - it.offsetZ = blockEntity.playerOffset.z - it.forwardX = blockEntity.playerForward.x - it.forwardY = blockEntity.playerForward.y - it.forwardZ = blockEntity.playerForward.z - it.forwardDynamic = blockEntity.playerForwardDynamic - it.forwardTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerForwardTrack) - it.positionDynamic = blockEntity.playerPositionDynamic - it.positionTrack = BlockTestAnimationTrackCodec.encode(blockEntity.playerPositionTrack) - it.boxWidth = blockEntity.playerBoxWidth - it.boxHeight = blockEntity.playerBoxHeight - it.boxDepth = blockEntity.playerBoxDepth - it.running = blockEntity.isRunning() - it.status = blockEntity.statusText() - it.optionCount = blockEntity.optionCount() - it.optionIds = ArrayList(blockEntity.optionIds()) - it.optionParamSpecs = encodeOptionParamSpecs(blockEntity.optionParamSpecs()) - it.optionParamValues = TestOptionParamCodec.encodeAllOptionValues(blockEntity.optionParamValues()) - it.pendingReview = blockEntity.hasPendingReview() - it.currentIndex = blockEntity.currentIndex() - it.registeredIds = registeredIds.map(ResourceLocation::toString) - it.registeredOptionIds = ArrayList(registeredIds.map { id -> - encodeOptionIds(blockEntity.optionIds(id)) - }) - it.registeredOptionParamSpecs = ArrayList(registeredIds.map { id -> - encodeOptionParamSpecs(blockEntity.optionParamSpecs(id)) - }) - } - } - - private fun encodeOptionIds(optionIds: List): String { - return optionIds.joinToString(OPTION_ID_SEPARATOR) - } - - private fun encodeOptionParamSpecs(optionSpecs: List>>): List { - return TestOptionParamCodec.encodeAllOptionSpecs(optionSpecs).map { encoded -> - encoded.encodeToByteArray() - } - } - - const val OPTION_ID_SEPARATOR: String = "\u001F" - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt deleted file mode 100644 index 3d3ac4b0..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketReviewTestControllerC2S.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import cn.coostack.cooparticlesapi.test.block.BlockTestOptionResult -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketReviewTestControllerC2S() : CooPacket() { - @CodecField var action: String = "" - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos, action: String) : this() { - this.dimension = dimension - this.blockPos = blockPos - this.action = action - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "review_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - val result = when { - action.equals(PASS, ignoreCase = true) -> BlockTestOptionResult.PASSED - action.equals(FAIL, ignoreCase = true) -> BlockTestOptionResult.FAILED - action.equals(SKIP, ignoreCase = true) -> BlockTestOptionResult.SKIPPED - else -> return - } - TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.reviewCurrent(result) - } - - companion object { - const val PASS = "pass" - const val FAIL = "fail" - const val SKIP = "skip" - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt deleted file mode 100644 index 39c2c610..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketSelectBoundTestControllerC2S.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketSelectBoundTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos) : this() { - this.dimension = dimension - this.blockPos = blockPos - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "select_bound_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return - TestControllerBlockAccess.openControllerScreen(sender, blockEntity) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt deleted file mode 100644 index 39a58572..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStartTestControllerC2S.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketStartTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos) : this() { - this.dimension = dimension - this.blockPos = blockPos - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "start_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.startTest() - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt deleted file mode 100644 index a0389b03..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketStopTestControllerC2S.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation - -@CooAutoRegister -class PacketStopTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var dimension: String = "" - - constructor(dimension: String, blockPos: BlockPos) : this() { - this.dimension = dimension - this.blockPos = blockPos - } - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "stop_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - TestControllerBlockAccess.find(sender.server, dimension, blockPos)?.stopTest() - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt deleted file mode 100644 index 5fe02c17..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/packet/testblock/PacketUpdateTestControllerC2S.kt +++ /dev/null @@ -1,80 +0,0 @@ -package cn.coostack.cooparticlesapi.network.packet.testblock - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CodecField -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.blocks.TestControllerBlockAccess -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket -import cn.coostack.cooparticlesapi.network.packet.api.ServerContext -import cn.coostack.cooparticlesapi.test.api.TestOptionParamCodec -import cn.coostack.cooparticlesapi.test.block.BlockTestAnimationTrackCodec -import cn.coostack.cooparticlesapi.test.block.BlockTestMode -import net.minecraft.core.BlockPos -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.phys.Vec3 - -@CooAutoRegister -class PacketUpdateTestControllerC2S() : CooPacket() { - @CodecField var blockPos: BlockPos = BlockPos.ZERO - @CodecField var boxDepth: Double = 0.6 - @CodecField var boxHeight: Double = 1.8 - @CodecField var boxWidth: Double = 0.6 - @CodecField var dimension: String = "" - @CodecField var forwardX: Double = 0.0 - @CodecField var forwardY: Double = 0.0 - @CodecField var forwardZ: Double = 1.0 - @CodecField var forwardDynamic: Boolean = false - @CodecField var forwardTrack: String = "" - @CodecField var groupId: String = "" - @CodecField var mode: String = "sequential" - @CodecField var offsetX: Double = 0.0 - @CodecField var offsetY: Double = 0.0 - @CodecField var offsetZ: Double = 0.0 - @CodecField var optionParamIndex: Int = 0 - @CodecField var optionParamValues: String = "" - @CodecField var positionDynamic: Boolean = false - @CodecField var positionTrack: String = "" - @CodecField var repeatDelayTicks: Int = 0 - @CodecField var repeatIndex: Boolean = false - @CodecField var reopen: Boolean = false - @CodecField var selectedIndex: Int = 0 - - override fun id(): ResourceLocation { - return ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, "update_test_controller_c2s") - } - - override fun onServerReceive(context: ServerContext) { - val sender = context.sender - if (!sender.isCreative) return - val blockEntity = TestControllerBlockAccess.find(sender.server, dimension, blockPos) ?: return - val wasRunning = blockEntity.isRunning() - // 待人工复核时重启会丢弃尚未给出的复核结果,交由随后的复核包决定测试组去向。 - val pendingReview = blockEntity.hasPendingReview() - val offset = Vec3(offsetX, offsetY, offsetZ) - val forward = Vec3(forwardX, forwardY, forwardZ) - val changed = blockEntity.updateConfig( - groupId = groupId, - mode = BlockTestMode.fromId(mode), - selectedIndex = selectedIndex, - repeatIndex = repeatIndex, - repeatDelayTicks = repeatDelayTicks, - playerOffset = offset, - playerForward = forward, - playerPositionDynamic = positionDynamic, - playerForwardDynamic = forwardDynamic, - playerPositionTrack = BlockTestAnimationTrackCodec.decode(positionTrack, offset), - playerForwardTrack = BlockTestAnimationTrackCodec.decode(forwardTrack, forward), - playerBoxWidth = boxWidth, - playerBoxHeight = boxHeight, - playerBoxDepth = boxDepth, - optionParamIndex = optionParamIndex, - optionParamValues = TestOptionParamCodec.decodeOptionValues(optionParamValues) - ) - if (wasRunning && changed && !pendingReview) { - blockEntity.startTest() - } - if (reopen) { - TestControllerBlockAccess.openControllerScreen(sender, blockEntity) - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt deleted file mode 100644 index 2f0ae949..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/SequencedServerParticleGroup.kt +++ /dev/null @@ -1,164 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.utils.MathDataUtil - -/** - * 由于服务端获取不到客户层的粒子总数 - * 所以这里不做粒子上限 - * 但是记录是用于 其他客户端同步 - * - * 如果要设定自动同步 - * 在 otherPacketArgs 设置 "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount) - * 或者 在这个方法中 return mapOf(toggleArgLeastIndex(),toggleArgStatus(),...其他自定义的数据) - * - * 你无需在 SequencedServerParticleGroup的Provider中 手动应用toggle输入的count - * 服务器会自动处理此参数 - * - * 数据包设定 - * addCount args type int -> count - * removeCount args type int -> count - * toggle args type int-> count - */ -@Deprecated("使用ParticleComposition") -abstract class SequencedServerParticleGroup(visibleRange: Double = 32.0) : ServerParticleGroup(visibleRange) { - /** - * 记录了每一个粒子的显示状态 (只要maxCount确实等于loadParticleLocationsWithIndex返回的map的size) - */ - private val clientIndexStatus = LongArray((maxCount() / 64) + 1) - - var serverSequencedParticleCount = 0 - - /** - * 切记一定要和 SequencedParticleGroup.loadParticleLocationsWithIndex().size 相同 - * - * 如果你的group的粒子数量是可变的(使用了flush方法刷新了粒子样式 其中长度发生变化) - * - * 那么请在服务器层做好数据同步 ( size同步 ) - * - * 如果此处的 maxCount > SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致数组越界异常 - * - * 如果此处的 maxCount < SequencedParticleGroup.loadParticleLocationsWithIndex().size 则会导致粒子控制不完全(部分粒子无法从服务器生成) - * - * @return 应当返回 SequencedParticleGroup.loadParticleLocationsWithIndex().size - */ - abstract fun maxCount(): Int - - fun isDisplayed(index: Int): Boolean { - val storage = MathDataUtil.getStoragePageLong(index) - if (storage >= clientIndexStatus.size) { - return false - } - - return MathDataUtil.getStorageWithBitLong( - MathDataUtil.getStorageWithBitLong(index) - ) == 1 - } - - fun setDisplayed(index: Int, status: Boolean) { - val storage = MathDataUtil.getStorageWithBitLong(index) - val page = MathDataUtil.getStoragePageLong(index) - if (page >= clientIndexStatus.size) { - return - } - clientIndexStatus[page] = MathDataUtil.setStatusLong(clientIndexStatus[page], storage, status) - } - - fun toggleArgLeastIndex(): Pair> { - return "toggle" to ParticleControlerDataBuffers.int(1) - } - - fun toggleArgStatus(): Pair> { - return "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) - } - - fun addSingle() { - if (serverSequencedParticleCount >= maxCount()) { - return - } - change( - { - setDisplayed(serverSequencedParticleCount++, true) - }, - mapOf( - "addCount" to ParticleControlerDataBuffers.int(1) - ) - ) - } - - fun addMultiple(count: Int) { - change( - { - for (index in serverSequencedParticleCount until serverSequencedParticleCount + count) { - setDisplayed(index, true) - } - serverSequencedParticleCount += count - }, - mapOf( - "addCount" to ParticleControlerDataBuffers.int(count) - ) - ) - if (serverSequencedParticleCount >= maxCount()) { - serverSequencedParticleCount = maxCount() - } - } - - fun removeSingle(): Boolean { - if (serverSequencedParticleCount <= 0) { - return false - } - change( - { - setDisplayed(serverSequencedParticleCount--, false) - }, - mapOf( - "removeCount" to ParticleControlerDataBuffers.int(1) - ) - ) - return true - } - - fun removeMultiple(count: Int) { - if (serverSequencedParticleCount <= 0) { - return - } - change( - { - for (index in serverSequencedParticleCount - count + 1..serverSequencedParticleCount) { - setDisplayed(index, false) - } - serverSequencedParticleCount -= count - if (serverSequencedParticleCount <= 0) { - serverSequencedParticleCount = 0 - } - }, - mapOf( - "removeCount" to ParticleControlerDataBuffers.int(count) - ) - ) - } - - fun changeSingle(index: Int, status: Boolean) { - if (index >= maxCount() || index < 0) { - return - } - change( - { - setDisplayed(index, status) - }, mapOf( - "change_single" to ParticleControlerDataBuffers.int(index) - ) - ) - } - - fun toggleCurrentCount() { - change( - {}, - mapOf( - "toggle" to ParticleControlerDataBuffers.int(serverSequencedParticleCount), - "toggle_status" to ParticleControlerDataBuffers.longArray(clientIndexStatus) - ) - ) - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt deleted file mode 100644 index f515823d..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroup.kt +++ /dev/null @@ -1,328 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.api.controler.server.ServerControler -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.entity.LivingEntity -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -/** - * @param clientGroup 客户端渲染的类型 - * @param uuid 粒子组合唯一标识符 - * @param visibleRange 玩家可见范围 (origin) - */ -@Deprecated("使用ParticleComposition") - -abstract class ServerParticleGroup( - var visibleRange: Double = 32.0 -) : ServerControler { - val uuid: UUID = UUID.randomUUID() - var pos: Vec3 = Vec3.ZERO - internal set - var world: Level? = null - internal set - internal var valid = true - var canceled = false - internal set - - /** - * 客户端渲染视角下的生命周期 - */ - var clientTick = 0 - internal set - var clientMaxTick = 120 - internal set - var scale = 1.0 - - /** - * 服务器内的生命周期 - * 同步数据时不会传输给客户端 - */ - var tick = 0 - var maxTick = 120 - - - var axis: RelativeLocation = RelativeLocation(0.0, 1.0, 0.0) - internal set - - /** - * 每个tick能做的事情 - * 例如绑定实体位置 旋转等 - */ - abstract fun tick() - - /** - * 获取自定义包的其他参数 (用于服务器传输给客户端) - */ - abstract fun otherPacketArgs(): Map> - - /** - * 获取对应的客户端层Class - * @return null 不能单独生成的类型 - */ - abstract fun getClientType(): Class? - - - /** - * 当粒子组合在服务器创建并且发送到客户端时执行 - * 可以给客户端发送一些包修改 - */ - open fun onGroupDisplay(pos: Vec3, world: ServerLevel) {} - - /** - * 通过doTickAlive函数导致粒子组合到达最终生命周期时执行的函数 - * 不强制重写 - */ - open fun onTickAliveDeath() {} - - /** - * 同步后客户端层面粒子死亡 (除非没开启tickAlive) - * 服务器处理上同样 - */ - open fun onClientViewDeath() {} - - /** - * 可以在abstract tick中执行 - */ - fun doTickClient() { - if (clientTick++ >= clientMaxTick) { - onClientViewDeath() - } - } - - /** - * 可以在abstract tick中执行 - */ - fun doTickAlive() { - if (tick++ >= maxTick) { - kill() - onTickAliveDeath() - } - } - - /** - * 不立刻同步给客户端 - * 只修改参数 - * 但是在新加入可视的玩家会应用此scale - * 如果想要在客户端不应用服务器的scale - * 则需要在provider的create方法中重新修改scale值 - */ - fun scaleOnServer(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale must be greater than zero.") - return - } - this.scale = new - } - - /** - * 服务器修改客户端缩放大小 - * 并且立刻同步到客户端 - */ - fun scale(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale must be greater than zero.") - return - } - change( - { - this.scale = new - }, - mapOf( - PacketParticleGroupS2C.PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(new) - ) - ) - } - - /** - * 销毁包 (在update时会删除所有的可见) - */ - fun kill() { - canceled = true - world ?: return - val visible = ServerParticleGroupManager.filterVisiblePlayer(this) - visible.forEach { - val player = world!!.getPlayerByUUID(it) ?: return@forEach - // 发销毁包 - val packet = PacketParticleGroupS2C(uuid, ControlType.REMOVE, mapOf()) - CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) - } - } - - /** - * 跟随玩家在线状态. 世界跟换 删除粒子 - */ - fun withPlayerStats(player: ServerPlayer) { - if (player.hasDisconnected()) { - kill() - } - - if (player.level() != world) { - kill() - } - } - - fun withEntityStats(entity: LivingEntity) { - if (entity.isDeadOrDying) { - kill() - } - - if (entity.level() != world) { - kill() - } - - } - - fun setAxis(axis: Vec3) { - change( - { this.axis = RelativeLocation.of(axis) }, mapOf( - PacketParticleGroupS2C.PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(axis) - ) - ) - } - - /** - * 不发送到客户端 - */ - fun setPosOnServer(pos: Vec3) { - this.pos = pos - } - - /** - * 同步旋转后的中心点和对称轴 - */ - fun setRotateToOnServer(to: RelativeLocation) { - axis = to.normalize() - } - - - /** - * 出现了一些控制类之外的传送 - */ - fun teleportGroupTo(pos: Vec3) { - // 发包告知 所有可见客户端 - change( - { this.pos = pos }, mapOf( - PacketParticleGroupS2C.PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(pos) - ) - ) - } - - override fun getValue(): ServerParticleGroup { - return this - } - - override fun remove() { - kill() - } - - - override fun teleportTo(to: Vec3) { - teleportGroupTo(to) - } - - override fun teleportTo(x: Double, y: Double, z: Double) { - teleportTo(Vec3(x, y, z)) - } - - /** - * 出现了一些控制类之外的旋转 - */ - override fun rotateAsAxis(angle: Double) { - change( - {}, mapOf( - PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( - angle - ) - ) - ) - } - - override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { - change( - {}, mapOf( - PacketParticleGroupS2C.PacketArgsType.ROTATE_AXIS.ofArgs to ParticleControlerDataBuffers.double( - angle - ), - PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d( - to.toVector() - ) - ) - ) - } - - override fun spawn(world: Level, pos: Vec3) { - if (world !is ServerLevel) return - ServerParticleGroupManager.addParticleGroup(this, pos, world) - } - - /** - * 出现了一些控制类之外的旋转 - */ - override fun rotateToPoint(to: RelativeLocation) { - rotateParticlesToPoint(to.toVector()) - } - - fun rotateParticlesToPoint(to: Vec3) { - change( - {}, mapOf( - PacketParticleGroupS2C.PacketArgsType.ROTATE_TO.ofArgs to ParticleControlerDataBuffers.vec3d(to) - ) - ) - } - - fun changeTick(tick: Int) { - change( - { clientTick = tick }, mapOf( - PacketParticleGroupS2C.PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int( - tick - ) - ) - ) - } - - fun changeMaxTick(tick: Int) { - change( - { this.maxTick = tick }, mapOf( - PacketParticleGroupS2C.PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(tick) - ) - ) - } - - /** - * 发送行为修改包 - * @param toggleMethod 修改Server层的参数的方法 如果不用此修改方法则会导致其他客户端渲染不同步 - * @param args 此参数出现过的键 必须在otherPacketArgs方法返回的args中存在 否则任然会存在同步问题 - */ - fun change(toggleMethod: ServerParticleGroup.() -> Unit, args: Map>) { - world ?: return - val visible = ServerParticleGroupManager.filterVisiblePlayer(this) - toggleMethod(this) - visible.forEach { - val player = world!!.getPlayerByUUID(it) ?: return@forEach - val packet = PacketParticleGroupS2C(uuid, ControlType.CHANGE, args) - CooParticlesServices.SERVER_NETWORK.send(packet, player as ServerPlayer) - } - } - - fun initServerGroup(pos: Vec3, world: Level, maxTick: Int = 120) { - this.pos = pos - this.world = world - this.clientMaxTick = maxTick - } - - override fun isValid(): Boolean { - return valid - } - -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt deleted file mode 100644 index 1588441b..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/ServerParticleGroupManager.kt +++ /dev/null @@ -1,200 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle - -import cn.coostack.cooparticlesapi.CooParticlesAPI -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleGroupS2C.PacketArgsType -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.phys.Vec3 -import java.util.* -import java.util.concurrent.ConcurrentHashMap - - -/** - * 控制所有的ServerParticleGroup - */ -@Deprecated("使用ParticleComposition") - -object ServerParticleGroupManager { - private val serverGroups = ConcurrentHashMap() - - /** - * key playerUUID - * value player can see - */ - internal val visible = ConcurrentHashMap>() - - fun addParticleGroup( - group: ServerParticleGroup, - pos: Vec3, - world: ServerLevel - ) { - serverGroups[group.uuid] = group - group.initServerGroup(pos, world) - world.players().filter { it.position().distanceTo(pos) <= group.visibleRange } - .forEach { receiver -> - addGroupPlayerView(receiver, group) - } - group.onGroupDisplay(pos, world) - } - - fun getParticleGroup(group: UUID): ServerParticleGroup? { - return serverGroups[group] - } - - fun getGroups(): Map { - return Collections.unmodifiableMap(serverGroups) - } - - /** 返回服务端当前旧 ParticleGroup 实例数。 */ - fun groupCount(): Int = serverGroups.size - - fun clearServer() { - serverGroups.onEach { it.value.canceled = true }.clear() - visible.clear() - } - - fun upgrade() { - upgradeGroups() - clearOfflineVisible() - } - - fun filterVisiblePlayer(group: ServerParticleGroup): Set { - val set = HashSet() - visible.forEach { - if (group in it.value) { - set.add(it.key) - } - } - return set - } - - private fun upgradeGroups() { - val iterator = serverGroups.iterator() - while (iterator.hasNext()) { - val value = iterator.next().value - // 更新状态 - if (value.canceled || !value.valid) { - val server = value.world?.server ?: continue - visible.forEach { (t, u) -> - val player = server.playerList.getPlayer(t) ?: return@forEach - if (value in u) { - removeGroupPlayerView(player, value) - u.remove(value) - } - } - iterator.remove() - continue - } - // 更新可见性 - value.world!!.server!!.playerList.players.forEach { p -> - val visibleSet = visible.getOrPut(p.uuid) { HashSet() } - if (p.level().dimension() != value.world?.dimension()) { - // 世界转换 - if (value in visibleSet) { - removeGroupPlayerView(p, value) - } - visibleSet!!.remove(value) - return@forEach - } - if (p.isDeadOrDying) { - if (value in visibleSet) { - removeGroupPlayerView(p, value) - visibleSet!!.remove(value) - } - return@forEach - } - if (value.pos.distanceTo(p.position()) <= value.visibleRange) { - // 防止重复添加(发包) - if (value in visibleSet) { - return@forEach - } - addGroupPlayerView(p, value) - // 同步数据包 - togglePacketView(p, value) - - } else { - if (value in visibleSet) { - removeGroupPlayerView(p, value) - } - // 超过范围 - visibleSet.remove(value) - } - } - value.tick() - } - } - - private fun clearOfflineVisible() { - val server = CooParticlesAPI.serverOrNull ?: return - // 清空所有离线玩家 - val visibleIterator = visible.iterator() - while (visibleIterator.hasNext()) { - val entry = visibleIterator.next() - val player = server.playerList.getPlayer(entry.key) - if (player == null || player.hasDisconnected()) { - visibleIterator.remove() - } - } - } - - private fun togglePacketView(target: ServerPlayer, group: ServerParticleGroup) { - val packet = PacketParticleGroupS2C( - group.uuid, ControlType.CHANGE, - mutableMapOf( - PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(group.pos), - PacketArgsType.AXIS.ofArgs to ParticleControlerDataBuffers.vec3d(group.axis.toVector()), - PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientTick), - PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(group.clientMaxTick), - ) - ) - - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - /** - * 在该玩家的视角中移除这些粒子 - * @param target - * @param targetGroup - */ - private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ServerParticleGroup) { - // 发包给玩家 - val packet = PacketParticleGroupS2C( - targetGroup.uuid, - ControlType.REMOVE, mapOf() - ) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - /* - * @param target - * @param targetGroup - */ - private fun addGroupPlayerView( - target: ServerPlayer, - targetGroup: ServerParticleGroup, - ) { - // 修复一个包发送给一个玩家两次 - val visibleSet = visible.getOrPut(target.uuid) { HashSet() } - visibleSet.add(targetGroup) - // 发包给玩家 - val packet = PacketParticleGroupS2C( - targetGroup.uuid, - ControlType.CREATE, - mutableMapOf( - PacketArgsType.POS.ofArgs to ParticleControlerDataBuffers.vec3d(targetGroup.pos), - PacketArgsType.GROUP_TYPE.ofArgs to ParticleControlerDataBuffers.string(targetGroup.getClientType()!!.name), - PacketArgsType.CURRENT_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientTick), - PacketArgsType.MAX_TICK.ofArgs to ParticleControlerDataBuffers.int(targetGroup.clientMaxTick), - PacketArgsType.SCALE.ofArgs to ParticleControlerDataBuffers.double(targetGroup.scale) - ).apply { - putAll(targetGroup.otherPacketArgs()) - } - ) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - -} 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 index aeff8e9c..ee2b9e6f 100644 --- 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 @@ -2,17 +2,14 @@ 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(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + override fun getCodec(): CommonStreamCodec { return ParticleCompositionRegistryHelper.generateCodec(this) } -} \ No newline at end of file +} 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 index 548bdf6b..06a55930 100644 --- 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 @@ -2,15 +2,14 @@ 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) { +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(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + override fun getCodec(): CommonStreamCodec { return ParticleCompositionRegistryHelper.generateCodec(this) } - -} \ No newline at end of file +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt deleted file mode 100644 index 2b7be814..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/composition/CompositionData.kt +++ /dev/null @@ -1,51 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.composition - -import cn.coostack.cooparticlesapi.cparticle.CParticle -import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.particles.control.ParticleControler -import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect -import java.util.UUID - -class CompositionData : Comparable { - var order = 0 - val uuid = UUID.randomUUID() - val singleParticleHandlers = ArrayList Unit>() - val particleControlerHandlers = ArrayList Unit>() - val cParticleHandlers = ArrayList Unit>() - val cParticleControlerHandlers = ArrayList Unit>() - var displayerBuilder: (UUID) -> ParticleDisplayer = { - ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) - } - private set - - fun setDisplayerSupplier(supplier: (UUID) -> ParticleDisplayer): CompositionData { - displayerBuilder = supplier - return this - } - - fun addParticleInstanceInit(init: ControlableParticle.() -> Unit): CompositionData { - singleParticleHandlers.add(init) - return this - } - - fun addParticleControlerInstanceInit(init: ParticleControler.() -> Unit): CompositionData { - particleControlerHandlers.add(init) - return this - } - - fun addCParticleInstanceInit(init: CParticle.() -> Unit): CompositionData { - cParticleHandlers.add(init) - return this - } - - fun addCParticleControlerInstanceInit(init: CParticleControlable.() -> Unit): CompositionData { - cParticleControlerHandlers.add(init) - return this - } - - override fun compareTo(other: CompositionData): Int { - return order - other.order - } -} 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 index aa643918..3838dbfe 100644 --- 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 @@ -30,7 +30,6 @@ import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionStat 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 @@ -41,16 +40,6 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.PI -/** - * 因为不想写太多的ControlableBuffer, 而改用Codec自动生成数据 (同时避免写一万个Provider内部类) - * - * 所以重构ParticleStyle - * - * 首先强制 auto update - * - * 使用自动注册 - * @see cn.coostack.cooparticlesapi.annotations.composition.ParticleCompositionRegister - */ abstract class ParticleComposition : ServerControler, Controlable, Tickable, NetworkDirtyMarkable { companion object { @@ -97,15 +86,11 @@ abstract class ParticleComposition : ServerControler, 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 @@ -123,9 +108,6 @@ abstract class ParticleComposition : ServerControler, var canceled = false - /** - * 如果作为子 composition输入就一定要修改这个 - */ var controlUUID = UUID.randomUUID() var axis = RelativeLocation.yAxis() @@ -133,24 +115,11 @@ abstract class ParticleComposition : ServerControler, var roll = 0.0 val particles = ConcurrentHashMap>() - - /** - * 排除掉 SingleParticleDisplayer 从而减少遍历次数 - */ val controlerTicks = HashSet>() - val particleLocations = ConcurrentHashMap, RelativeLocation>() - val status = CompositionStatusHelper() - - /** 当粒子组合初始化时,存储一倍缩放粒子组与原点的距离。 */ val particleDefaultLength = ConcurrentHashMap() - - /** 保留一倍缩放时的相对向量,使零缩放后的粒子能够恢复方向。 */ private val particleDefaultLocations = ConcurrentHashMap() - - // 防止频繁的toList造成的性能浪费 - internal val invokeQueue = ArrayList Unit>() internal val postInvokeQueue = ArrayList Unit>() protected val particleRotatedLocations = ArrayList() @@ -171,11 +140,6 @@ abstract class ParticleComposition : ServerControler, private var lastNetworkStatus = status.displayStatus private var lastNetworkStatusInterval = status.closedInternal - /** - * 标记此 Composition 的完整网络状态需要重新同步。 - * - * 自定义同步字段优先使用 `var value by dirty(initial)`;普通 `@CodecField` 修改后需要显式调用本方法。 - */ override fun markDirty() { if (world?.isClientSide != true) { networkFullDirty = true @@ -235,33 +199,22 @@ abstract class ParticleComposition : ServerControler, status.updateCurrent(current) } - abstract fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + abstract fun getCodec(): CommonStreamCodec abstract fun getParticles(): Map abstract fun onDisplay() - /** - * 快捷设置 链式调用 - * - * @param interval - * @return - */ fun setDisabledInterval(interval: Int): ParticleComposition { this.status.closedInternal = interval markNetworkStateDirty() return this } - /** - * 配置此 composition 自动创建的所有 CParticle systems。 - * 名称、容量、渲染层、模式和生命周期由 composition 管理。 - */ fun configureCParticleSystem(configure: CParticleSystem.() -> Unit): ParticleComposition { return setCParticleSystemConfiguration(null, configure) } - /** 只配置指定渲染层的 system。 */ fun configureCParticleSystem( layer: CParticleRenderLayer, configure: CParticleSystem.() -> Unit, @@ -280,12 +233,10 @@ abstract class ParticleComposition : ServerControler, return this } - /** 返回此 composition 在指定层自动创建的 system;尚未显示时返回 null。 */ fun getCParticleSystem(layer: CParticleRenderLayer): CParticleSystem? { return managedCParticleSystems.firstOrNull { !it.released && it.layer == layer } } - /** 返回此 composition 当前持有的 systems 快照。 */ fun getCParticleSystems(): List { removeReleasedCParticleSystems() return referencedCParticleSystems.keys.toList() @@ -336,20 +287,6 @@ abstract class ParticleComposition : ServerControler, } } - /** - * 对当前 composition 创建的所有 CParticle systems 播放同一段 GPU 视觉过渡。 - * - * 示例:`playCParticleVisualTransition(20F, scaleCurve = curve)` 会统一缩放当前 systems。 - * 禁止:不要用它修改粒子生成时的基础尺寸。 - * - * @param durationTicks 过渡时长,单位 tick - * @param alphaCurve 不透明度倍率曲线 - * @param scaleCurve 等比缩放倍率曲线 - * @param colorFrom 可选起始颜色 - * @param colorTo 可选结束颜色 - * @param mode 过渡结束后的行为 - * @return 当前 composition - */ @JvmOverloads fun playCParticleVisualTransition( durationTicks: Float, @@ -370,21 +307,6 @@ abstract class ParticleComposition : ServerControler, ) } - /** - * 播放 composition 级 GPU 视觉过渡,并允许强制重新开始相同配置。 - * - * 示例:`playCParticleVisualTransition(20F, true, scaleCurve = curve)` 会重置进度。 - * 禁止:不要用它修改粒子生成时的基础尺寸。 - * - * @param durationTicks 过渡时长,单位 tick - * @param restart 是否强制从头播放 - * @param alphaCurve 不透明度倍率曲线 - * @param scaleCurve 等比缩放倍率曲线 - * @param colorFrom 可选起始颜色 - * @param colorTo 可选结束颜色 - * @param mode 过渡结束后的行为 - * @return 当前 composition - */ @JvmOverloads fun playCParticleVisualTransition( durationTicks: Float, @@ -406,21 +328,6 @@ abstract class ParticleComposition : ServerControler, ) } - /** - * 把同一组过渡参数分发给当前 composition 管理的 systems。 - * - * 示例:公开重载通过本方法统一传递 [restart]。 - * 禁止:不要在这里缓存已经释放的 system。 - * - * @param durationTicks 过渡时长,单位 tick - * @param alphaCurve 不透明度倍率曲线 - * @param scaleCurve 等比缩放倍率曲线 - * @param colorFrom 可选起始颜色 - * @param colorTo 可选结束颜色 - * @param mode 过渡结束后的行为 - * @param restart 是否强制从头播放 - * @return 当前 composition - */ private fun playCParticleVisualTransitionInternal( durationTicks: Float, alphaCurve: CParticleCurve?, @@ -444,25 +351,12 @@ abstract class ParticleComposition : ServerControler, return this } - /** 停止当前 composition 的 CParticle GPU 视觉过渡。 */ @JvmOverloads fun stopCParticleVisualTransition(reset: Boolean = false): ParticleComposition { getCParticleSystems().forEach { it.stopVisualTransition(reset) } return this } - /** - * 扫描此 composition 的完整运行时子树,为所有 CParticle systems 播放同一段 GPU alpha 过渡。 - * 子树包括嵌套 composition、ParticleGroupStyle 和 ControlableParticleGroup。 - * 过渡曲线会覆盖粒子实例的 alpha,再与粒子、system 和普通视觉过渡的 alpha 曲线相乘。 - * - * @param durationTicks 过渡时长,单位为 tick,必须为有限正数 - * @param alphaCurve 用于覆盖粒子实例 alpha 的曲线 - * @param mode 过渡结束后的行为 - * @param restart 是否强制替换相同配置 - * @return 当前 composition - * @throws IllegalArgumentException 当 [durationTicks] 不是有限正数时抛出 - */ @JvmOverloads fun playCParticleAlphaTransition( durationTicks: Float, @@ -479,8 +373,6 @@ abstract class ParticleComposition : ServerControler, ) } - /** 停止此 composition 完整运行时子树中的 CParticle alpha 过渡。 */ - @JvmOverloads fun stopCParticleAlphaTransition(reset: Boolean = false): ParticleComposition { return CParticleCompositionAlphaHelper.stop(this, reset) } @@ -524,8 +416,6 @@ abstract class ParticleComposition : ServerControler, if (scale == new) return scale = new markNetworkStateDirty() - // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 - // 多为服务端(除非有人使在Client环境创建了这个类) if (displayed) { if (gpuTransformActive) { applyGpuScale(new) @@ -533,9 +423,7 @@ abstract class ParticleComposition : ServerControler, toggleScaleDisplayed() } } - // 发包有效 if (!canceled) { - // remove过后 无法同步 return } } @@ -572,13 +460,11 @@ abstract class ParticleComposition : ServerControler, 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) @@ -602,14 +488,6 @@ abstract class ParticleComposition : ServerControler, return this } - /** - * 清除当前显示内容,并按 [cancel] 决定是否结束本次生命周期。 - * - * 示例:`clear(true)` 会销毁子节点并从客户端活动计数中移除本实例。 - * 禁止用 `clear(false)` 表示最终移除,因为该模式用于保留 displayed 状态后重新生成内容。 - * - * @param cancel `true` 表示结束生命周期,`false` 表示仅刷新显示内容 - */ open fun clear(cancel: Boolean) { particles.forEach { it.value.remove() @@ -628,12 +506,6 @@ abstract class ParticleComposition : ServerControler, } } - /** - * 在服务端重新生成 Composition 前重置旧生命周期状态。 - * - * 示例:同一实例再次调用 `spawn(...)` 时先执行本方法,避免沿用 canceled 状态。 - * 禁止在仍需保留当前客户端显示内容时调用。 - */ internal fun resetLifecycleForSpawn() { ParticleCompositionManager.setClientLoaded(this, false) canceled = false @@ -644,12 +516,6 @@ abstract class ParticleComposition : ServerControler, lastNetworkStatusInterval = status.closedInternal } - /** - * 根据当前世界进入服务端同步或客户端显示生命周期。 - * - * 示例:客户端直接显示的嵌套 Composition 会在此登记为活动实例。 - * 禁止在 [world] 尚未设置时调用。 - */ open fun display() { if (displayed) { return @@ -659,12 +525,10 @@ abstract class ParticleComposition : ServerControler, if (client) { ParticleCompositionManager.setClientLoaded(this, true) } - // 在服务器需要用来更新粒子个数 所以需要参与一次计算 flush() status.loadControler(this) status.initHelper() if (!client) { - // 服务器只负责数据同步 不负责粒子生成 onDisplay() return } @@ -684,7 +548,6 @@ abstract class ParticleComposition : ServerControler, } } - /** 按一倍缩放缓存恢复方向后应用当前缩放,避免从零缩放恢复时产生 NaN。 */ protected fun applyScale(uuid: UUID, location: RelativeLocation) { val defaultLength = particleDefaultLength[uuid] ?: return if (defaultLength <= 0.0) return @@ -719,7 +582,6 @@ abstract class ParticleComposition : ServerControler, staleControls.add(particle) continue } - // 减少一倍的new Vec3 try { particle.teleportTo( position.add(rel.x, rel.y, rel.z) @@ -836,7 +698,6 @@ abstract class ParticleComposition : ServerControler, override fun spawn(world: Level, pos: Vec3) { this.world = world this.position = pos - // display ParticleCompositionManager.spawn(this) } @@ -887,15 +748,6 @@ abstract class ParticleComposition : ServerControler, toggleRelative() } - /** - * 显示一个相对坐标对应的粒子节点。 - * - * 示例:普通粒子以 [position] 为原点,启用整组变换的 managed CParticle 先计算实际渲染坐标。 - * 禁止把 system 局部坐标直接当作世界坐标,否则光照和位置相关纹理会从错误位置采样。 - * - * @param data 粒子显示方式和初始化动作 - * @param pos 已完成初始缩放与旋转的相对坐标 - */ protected open fun displayEntry(data: CompositionData, pos: RelativeLocation) { val uuid = data.uuid val displayer = data.displayerBuilder(uuid) @@ -930,12 +782,6 @@ abstract class ParticleComposition : ServerControler, handler(controler) } } - if (controler is CParticleControlable) { - data.cParticleControlerHandlers.forEach { handler -> - handler(controler) - } - } - registerCParticleNode(controler) if (controler is CParticleControlable && controler.hasTickActions) { controlerTicks.add(controler) } else if (controler is Tickable<*> && controler !is CParticleControlable) { @@ -946,7 +792,6 @@ abstract class ParticleComposition : ServerControler, particleLocations[controler] = pos } - protected open fun displayParticles() { if (!client) { return @@ -973,15 +818,6 @@ abstract class ParticleComposition : ServerControler, particleRotatedLocations.add(pos) } - /** - * 为未绑定的 CParticle displayer 取得当前 composition 专用的 system。 - * - * 示例:同一 composition、渲染层和纹理绑定会复用同一个 system。 - * 禁止覆盖调用方已经绑定的 system;这种情况返回 `null` 并保留原有行为。 - * - * @param displayer 待绑定的 GPU 粒子显示器 - * @return 本次绑定的 managed system;无法绑定或已有外部绑定时返回 `null` - */ private fun bindManagedSystem(displayer: CParticleDisplayer): CParticleSystem? { if (displayer.hasBoundSystem) return null val layerName = displayer.layer.name.lowercase() @@ -1027,16 +863,6 @@ abstract class ParticleComposition : ServerControler, return target } - /** - * 计算粒子显示与环境采样使用的世界坐标。 - * - * 示例:整组变换启用后,序列后续粒子使用 system 当前矩阵换算真实渲染位置。 - * 禁止在这里返回槽位局部坐标;[CParticleSystem] 会在写入时自行逆变换。 - * - * @param pos composition 中保存的相对坐标 - * @param managedSystem 当前 composition 管理的 GPU system,普通显示器传 `null` - * @return 写入显示器的世界坐标 - */ internal fun resolveCParticleSpawnPosition( pos: RelativeLocation, managedSystem: CParticleSystem?, @@ -1054,16 +880,6 @@ abstract class ParticleComposition : ServerControler, ) } - /** - * 返回整组 GPU 变换启用时使用的稳定槽位坐标。 - * - * 示例:system 原点为 `(10, 20, 30)` 时,相对坐标 `(2, 3, 4)` 写为 `(12, 23, 34)`。 - * 禁止在 CPU 路径提供该坐标;此时 system 需要按现存矩阵逆变换真实世界坐标。 - * - * @param pos composition 保存的相对坐标 - * @param managedSystem 当前 composition 管理的 GPU system - * @return 槽位写入坐标;不使用整组 GPU 变换时返回 `null` - */ internal fun resolveCParticleStoragePosition( pos: RelativeLocation, managedSystem: CParticleSystem?, @@ -1072,9 +888,9 @@ abstract class ParticleComposition : ServerControler, return system.origin.add(pos.x, pos.y, pos.z) } - private fun applyGpuAxisRotation(rotationAxis: RelativeLocation, radian: Double) { + private fun applyGpuAxisRotation(rotationAxis: RelativeLocation, radian: Float) { val axisVector = normalizedAxis(rotationAxis) - val delta = Matrix4f().rotate(radian.toFloat(), axisVector) + val delta = Matrix4f().rotate(radian.toDouble(), axisVector) delta.mul(cParticleLinearTransform, cParticleLinearTransform) syncGpuTransform() } 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 index a91ef2fa..51b20c74 100644 --- 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 @@ -1,13 +1,6 @@ package cn.coostack.cooparticlesapi.network.particle.composition -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.StatusHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper import net.minecraft.network.PacketByteBuf - import net.minecraft.world.phys.Vec3 import java.util.UUID @@ -25,21 +18,9 @@ class ParticleShapeComposition(uuid: UUID) : ParticleComposition(Vec3.ZERO, null var scaleHelper: ScaleHelper? = null private set var spawnAge = 0 - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ var scalePreTick = false private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ var scaleReversed = false - - /** - * 当反转到0时会自动清理粒子 - */ var reversedClean = true fun loadScaleHelper(min: Double, max: Double, scalingTick: Int): ParticleShapeComposition { @@ -64,11 +45,6 @@ class ParticleShapeComposition(uuid: UUID) : ParticleComposition(Vec3.ZERO, null return this } - /** - * 改用apply是因为 要防止和addPreTickAction冲突 - * - * @return - */ fun applyDisplayAction(action: ParticleShapeComposition.() -> Unit): ParticleShapeComposition { invokes.add(action) return this @@ -113,8 +89,7 @@ class ParticleShapeComposition(uuid: UUID) : ParticleComposition(Vec3.ZERO, null return this } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + override fun getCodec(): CommonStreamCodec { throw NotImplementedError("此类只作为客户端嵌套使用, 不能单独生成! ") } @@ -153,4 +128,4 @@ class ParticleShapeComposition(uuid: UUID) : ParticleComposition(Vec3.ZERO, null } } } -} \ No newline at end of file +} 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 index 62dcde29..caf3b5e3 100644 --- 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 @@ -1,518 +1,13 @@ package cn.coostack.cooparticlesapi.network.particle.composition -import cn.coostack.cooparticlesapi.api.controler.Tickable -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.utils.Math3DUtil -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.helper.SequencedCompositionAnimationHelper -import cn.coostack.cooparticlesapi.utils.storage.Memo import net.minecraft.network.PacketByteBuf -import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.UUID -import kotlin.math.max -import kotlin.math.min -/** - * 自动更新(AutoToggle) 生长动画 - * - * AI神力 根本懒得再写一份Sequenced的版本了 - */ -abstract class SequencedParticleComposition(position: Vec3, world: Level? = null) : - ParticleComposition(position, world) { +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) - constructor(world: Level) : this(Vec3.ZERO, world) - constructor(world: Level, pos: Vec3) : this(pos, world) - - companion object { - @JvmStatic - fun encodeBase(data: SequencedParticleComposition, buf: PacketByteBuf) { - ParticleComposition.encodeBase(data, buf) - buf.writeInt(data.count) - buf.writeInt(data.displayedParticleCount) - buf.writeInt(data.serverCurrentIndex) - buf.writeLongArray(data.index.get()) - } - - @JvmStatic - fun decodeBase(instance: SequencedParticleComposition, buf: PacketByteBuf) { - ParticleComposition.decodeBase(instance, buf) - instance.count = buf.readInt() - instance.displayedParticleCount = buf.readInt() - instance.serverCurrentIndex = buf.readInt() - instance.index.setMemoValue(buf.readLongArray()) - } - } - - /** - * 这里采用 animate框架 写多个条件用来方便生长动画的条件变化 - * 在init 或者 onDisplay执行 - */ - val animate = - SequencedCompositionAnimationHelper() - .loadComposition(this) - - - var count = 0 - var index = Memo { - var page = count / 64 - if (count % 64 > 0) { - page++ - } - LongArray(page) - } - - /** 服务端:已经“显示”的粒子数量(仅逻辑字段,可用于业务) */ - var displayedParticleCount: Int = 0 - protected set - - /** 服务端:下一个将要生成的 index 指针(用于 addSingle/addMultiple 生长动画) */ - var serverCurrentIndex: Int = 0 - protected set - protected val sequencedParticlesData = ArrayList>() - override fun getParticles(): SortedMap { - return getParticleSequenced() - } - - /** - * 不要调用只有客户端才能调用的方法 因为服务器也会调用一次这个方法用来获取实际的粒子个数 - * - * @return 粒子相对样式 - */ - abstract fun getParticleSequenced(): SortedMap - final override fun tick() { - super.tick() - } - - override fun flush() { - if (particles.isNotEmpty() || indexToUuid.any { it != null }) { - clear(false) - } - displayParticles() - restoreDisplayedParticles() - } - - override fun clear(cancel: Boolean) { - super.clear(cancel) - sequencedParticlesData.clear() - particleRotatedLocations.clear() - indexToUuid.fill(null) - } - - /** - * 进入序列 Composition 的显示生命周期并初始化动画状态。 - * - * 示例:客户端直接显示时会登记为一个活动 Composition 实例。 - * 禁止在 [world] 尚未设置时调用。 - */ - override fun display() { - if (displayed) { - return - } - displayed = true - // 修复禁用后不自动移除的问题 - status.loadControler(this) - status.initHelper() - this.client = world!!.isClientSide - if (client) { - ParticleCompositionManager.setClientLoaded(this, true) - } - // 在服务器需要用来更新粒子个数 所以需要参与一次计算 - flush() - if (!client) { - // 服务器只负责数据同步,不负责粒子生成。 - onDisplay() - return - } - onDisplay() - } - - open fun beforeDisplaySequenced(map: SortedMap) { - - } - - /** - * 这里非常关键: - * - 只计算序列数据、count、缩放/旋转后的相对坐标 - * - 不创建粒子对象(不调用 displayEntry) - */ - override fun displayParticles() { - val locations = getParticles() - val newCount = locations.size - prepareGpuComposition(newCount) - - // 更新粒子总数并扩容位集,同时保留已有状态。 - ensureIndexCapacity(newCount) - - count = newCount - - // 服务端不做任何粒子生成 - if (!client) { - return - } - - beforeDisplaySequenced(locations) - Math3DUtil.rotatePointsToPoint(locations.values.toList(), axis, RelativeLocation.yAxis()) - Math3DUtil.rotateAsAxis(locations.values.toList(), axis, roll) - toggleScale(locations) - - sequencedParticlesData.clear() - sequencedParticlesData.addAll(locations.toList()) - particleRotatedLocations.clear() - particleRotatedLocations.addAll(locations.values) - // 客户端:准备索引到 UUID 的映射数组。 - if (indexToUuid.size != count) { - indexToUuid = arrayOfNulls(count) - } - } - - override fun update(other: ParticleComposition) { - super.update(other) - other as SequencedParticleComposition - val oldIndex = this.index.get().copyOf() - val oldCount = this.count - - this.count = other.count - this.displayedParticleCount = other.displayedParticleCount - this.serverCurrentIndex = other.serverCurrentIndex - - this.index.setMemoValue(other.index.get()) - - if (!client) return - - // 粒子总数或缓存结构变化时,客户端需要重新计算序列数据。 - if (oldCount != this.count || sequencedParticlesData.size != this.count) { - // 缓存重建会移除旧节点,flush 会按新的完整位集恢复已显示状态。 - flush() - return - } - - applyIndexDiff(oldIndex, this.index.get()) - restoreDisplayedParticles() - } - - private fun restoreDisplayedParticles() { - if (!client || sequencedParticlesData.size != count) return - for (i in 0 until count) { - if (isParticleDisplayed(i)) createWithIndex(i) - } - } - - private fun applyIndexDiff(oldBits: LongArray, newBits: LongArray) { - - if (!client) return - val n = count - if (n <= 0) return - if (sequencedParticlesData.size != n) return - - val pages = pagesFor(n) - // 复制到同长度,避免越界判断 - val oldSafe = if (oldBits.size == pages) oldBits else oldBits.copyOf(pages) - val newSafe = if (newBits.size == pages) newBits else newBits.copyOf(pages) - - for (page in 0 until pages) { - var diff = oldSafe[page] xor newSafe[page] - if (diff == 0L) continue - - val base = page shl 6 // 当前页的首个槽位 - while (diff != 0L) { - val bit = diff.countTrailingZeroBits() // 0..63 - val i = base + bit - if (i >= n) break - - val newGen = ((newSafe[page] ushr bit) and 1L) != 0L - if (newGen) createWithIndex(i) else removeWithIndex(i) - - diff = diff and (diff - 1) // 清除最低位的 1 - } - } - } - - - /** - * 服务端:生成下一个粒子(只改 位集 和计数,不生成粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * addSingle() - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断。 - * 方法从当前指针向后寻找第一个未显示槽位,必要时回绕;成功后会标记完整网络状态。 - * 服务端调用必须位于 Composition 生命周期更新所在的逻辑线程。 - */ - fun addSingle() { - if (count <= 0) return - val start = serverCurrentIndex.coerceIn(0, count - 1) - val idx = (start until count).firstOrNull { !isParticleDisplayed(it) } - ?: (0 until start).firstOrNull { !isParticleDisplayed(it) } - ?: return - - setBit(index.get(), idx, true) - displayedParticleCount++ - - // 客户端:立即生成该索引对应的粒子。 - if (client) { - createWithIndex(idx) - } - serverCurrentIndex = min(idx + 1, max(count - 1, 0)) - markDirty() - } - - /** - * 服务端:生成多个粒子(只改 位集 和计数,不生成粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * addMultiple(amount) - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * @param amount 生成数量(小于等于 0 时直接返回) - */ - fun addMultiple(amount: Int) { - if (amount <= 0) return - repeat(amount) { addSingle() } - } - - /** - * 服务端:移除上一个粒子(只改 位集 和计数,不删除粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * removeSingle() - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * 说明: - * - 从 `serverCurrentIndex` 向前寻找最近的已显示槽位,必要时回绕 - * - 成功后清除位、更新计数和指针,并标记完整网络状态 - * - 客户端场景会立即调用 `removeWithIndex`,服务端调用必须位于 Composition 生命周期更新线程 - */ - fun removeSingle() { - if (count <= 0) return - val start = serverCurrentIndex.coerceIn(0, count - 1) - val idx = (start downTo 0).firstOrNull(::isParticleDisplayed) - ?: (count - 1 downTo start + 1).firstOrNull(::isParticleDisplayed) - ?: return - - setBit(index.get(), idx, false) - displayedParticleCount-- - - // 客户端:立即删除该索引对应的粒子。 - if (client) { - removeWithIndex(idx) - } - serverCurrentIndex = max(idx - 1, 0) - markDirty() - } - - /** - * 服务端:移除多个粒子(只改 位集 和计数,不删除粒子对象) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * removeMultiple(amount) - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * @param amount 移除数量(小于等于 0 时直接返回) - */ - fun removeMultiple(amount: Int) { - if (amount <= 0) return - repeat(amount) { removeSingle() } - } - - /** - * 重置所有粒子状态(清空 位集、计数器、指针) - * - * 警告:如果你的样式不是仅客户端(也就是要在服务器里调用生成),在调用此方法前 - * 一定要在非客户端作用域执行 - * ```kotlin - * if (!client){ - * resetAll() - * } - * ``` - * 如果你使用的是单纯的客户端 比如 SequencedParticleShapeComposition 则无需此判断 - * - * 说明: - * - 服务端:只清位集、计数和指针,等待同步到客户端后按差异处理 - * - 客户端:先遍历位集,把已生成的粒子对象全部通过 `removeWithIndex(i)` 移除,再清空位集 - * - * 注意: - * - resetAll() 会把 serverCurrentIndex 重置为 0 - * - displayedParticleCount 会重置为 0 - */ - fun resetAll() { - // 客户端:先删除所有已经生成的粒子对象。 - if (client && count > 0) { - val pages = pagesFor(count) - val bits = index.get() - for (page in 0 until min(bits.size, pages)) { - var v = bits[page] - if (v == 0L) continue - val base = page shl 6 - while (v != 0L) { - val bit = v.countTrailingZeroBits() - val i = base + bit - if (i >= count) break - removeWithIndex(i) - v = v and (v - 1) - } - } - } - - // 清空位集。 - val arr = index.get() - val changed = displayedParticleCount != 0 || serverCurrentIndex != 0 || arr.any { it != 0L } - for (i in arr.indices) arr[i] = 0L - - displayedParticleCount = 0 - serverCurrentIndex = 0 - if (changed) markDirty() - } - - override fun rotateToPoint(to: RelativeLocation) { - super.rotateToPoint(to) - } - - override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { - super.rotateToWithAngle(to, radian) - } - - override fun rotateAsAxis(radian: Double) { - super.rotateAsAxis(radian) - } - - /** - * 直接设置一个序列槽位的显示状态。 - * - * 服务端真实变更后会标记完整网络状态,客户端真实变更后会立即创建或移除对应节点。 - * 越界或状态未变化时不会修改计数、节点和网络脏状态。 - * - * @param index 目标槽位索引,有效范围为 `0 until count` - * @param generated `true` 表示显示,`false` 表示移除 - */ - fun setParticleStatus(index: Int, generated: Boolean) { - if (index !in 0 until count || isParticleDisplayed(index) == generated) return - setBit(this.index.get(), index, generated) - displayedParticleCount += if (generated) 1 else -1 - if (client) { - if (generated) createWithIndex(index) else removeWithIndex(index) - } - markDirty() - } - - fun isParticleDisplayed(index: Int): Boolean { - if (index !in 0 until count) return false - return getBit(this.index.get(), index) - } - - final override fun toggleScaleDisplayed() { - if (!displayed) { - return - } - sequencedParticlesData.forEach { (data, location) -> - applyScale(data.uuid, location) - } - toggleRelative() - } - - /** - * 客户端使用的索引到 UUID 映射,用于按索引删除。 - * 服务器不会读取该映射。 - */ - private var indexToUuid: Array = emptyArray() - private fun createWithIndex(i: Int) { - if (!client) return - if (i !in 0 until sequencedParticlesData.size) return - if (indexToUuid.size != count) indexToUuid = arrayOfNulls(count) - - // 如果已经创建过就跳过(避免重复) - if (indexToUuid[i] != null) return - - val (data, rl) = sequencedParticlesData[i] - displayEntry(data, rl) - if (particles.containsKey(data.uuid)) { - indexToUuid[i] = data.uuid - } - } - - override fun displayEntry(data: CompositionData, pos: RelativeLocation) { - super.displayEntry(data, pos) - refreshGpuTransformMode() - } - - override fun trackDisplayedParticleLocation(pos: RelativeLocation) = Unit - - - private fun removeWithIndex(i: Int) { - if (!client) return - if (i !in 0 until count) return - if (indexToUuid.size != count) return - - val uuid = indexToUuid[i] ?: return - val obj = particles[uuid] ?: run { - indexToUuid[i] = null - return - } - - unregisterCParticleNode(obj) - obj.remove() - if (obj is Tickable<*>) { - controlerTicks.remove(obj) - } - particles.remove(uuid) - particleLocations.remove(obj) - indexToUuid[i] = null - refreshGpuTransformMode() - } - - - private fun ensureIndexCapacity(newCount: Int) { - val pages = pagesFor(newCount) - val current = index.get() - if (current.size == pages) return - - val resized = LongArray(pages) - // 把旧页复制到新数组 - val len = min(current.size, resized.size) - for (i in 0 until len) resized[i] = current[i] - index.setMemoValue(resized) - } - - private fun pagesFor(cnt: Int): Int { - if (cnt <= 0) return 0 - return (cnt + 63) / 64 - } - - private fun getBit(arr: LongArray, index: Int): Boolean { - if (arr.isEmpty()) return false - val page = index ushr 6 // /64 - if (page !in arr.indices) return false - val bit = index and 63 - return ((arr[page] ushr bit) and 1L) == 1L + override fun getCodec(): CommonStreamCodec { + TODO("Not yet implemented") } - - private fun setBit(arr: LongArray, index: Int, value: Boolean) { - if (arr.isEmpty()) return - val page = index ushr 6 - if (page !in arr.indices) return - val bit = index and 63 - val mask = 1L shl bit - arr[page] = if (value) (arr[page] or mask) else (arr[page] and mask.inv()) - } - } 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 index c6fc97c0..de0e41af 100644 --- 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 @@ -1,203 +1,11 @@ package cn.coostack.cooparticlesapi.network.particle.composition -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.StatusHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionBezierScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.composition.CompositionScaleHelper import net.minecraft.network.PacketByteBuf - import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.TreeMap import java.util.UUID -/** - * - 只用于客户端嵌套(不允许单独发包生成) - * - 继承 SequencedParticleComposition:提供 SortedMap 序列数据(按 CompositionData.order 排序) - */ -class SequencedParticleShapeComposition(uuid: UUID) : SequencedParticleComposition(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 - - var spawnAge = 0 - - /** - * 是否在reversed 到0的时候清理粒子 (防止残留影响观感) - */ - var reversedClean = true - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - private set - - fun loadScaleHelper(min: Double, max: Double, scalingTick: Int): SequencedParticleShapeComposition { - scaleHelper = CompositionScaleHelper(min, max, scalingTick).apply { - loadControler(this@SequencedParticleShapeComposition) - } - scalePreTick = true - return this - } - - fun loadScaleHelperBezierValue( - minScale: Double, - maxScale: Double, - scaleTick: Int, - c1: RelativeLocation, - c2: RelativeLocation - ): SequencedParticleShapeComposition { - scaleHelper = CompositionBezierScaleHelper(scaleTick, minScale, maxScale, c1, c2) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - /** - * 改用apply是因为 要防止和addPreTickAction冲突 - */ - fun applyDisplayAction(action: SequencedParticleShapeComposition.() -> Unit): SequencedParticleShapeComposition { - invokes.add(action) - return this - } - - fun applyBeforeDisplayAction( - action: SequencedParticleShapeComposition.(SortedMap) -> Unit - ): SequencedParticleShapeComposition { - beforeInvokes.add(action) - return this - } - - fun applyPoint( - point: RelativeLocation, - dataSupplier: (RelativeLocation, Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(PointsBuilder().addPoint(point) to dataSupplier) - return this - } - - fun applyPointRel( - point: RelativeLocation, - dataSupplier: (RelativeLocation) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(rel) }) - return this - } - - fun applyPointI( - point: RelativeLocation, - dataSupplier: (Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(PointsBuilder().addPoint(point) to { rel, o -> dataSupplier(o) }) - return this - } - - - fun applyBuilder( - builder: PointsBuilder, - dataSupplier: (RelativeLocation, Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(builder to dataSupplier) - return this - } - - fun applyBuilderI( - builder: PointsBuilder, - dataSupplier: (Int) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(builder to { rel, o -> dataSupplier(o) }) - return this - } - - fun applyBuilderRel( - builder: PointsBuilder, - dataSupplier: (RelativeLocation) -> CompositionData - ): SequencedParticleShapeComposition { - points.add(builder to { rel, o -> dataSupplier(rel) }) - return this - } - - fun setReversedScaleOnDisableStatus(status: StatusHelper): SequencedParticleShapeComposition { - addPreTickAction { - if (status.displayStatus == 2) { - scaleReversed = true - } - } - return this - } - - fun setReversedScaleOnCompositionStatus(composition: ParticleComposition): SequencedParticleShapeComposition { - addPreTickAction { - if (composition.status.getCurrentStatus() == StatusHelper.Status.DISABLE) { - scaleReversed = true - } - } - return this - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - throw NotImplementedError("此类只作为客户端嵌套使用, 不能单独生成! ") - } - - /** - * SequencedParticleComposition 需要返回 SortedMap - * TreeMap 会按 CompositionData.compareTo -> order 排序 - */ - override fun getParticleSequenced(): SortedMap { - val res: SortedMap = TreeMap() - var order = 0 - points.forEach { (builder, supplier) -> - res.putAll( - builder.createWithCompositionData { rel -> - supplier(rel, order++) - } - ) - } - return res - } - - - override fun beforeDisplaySequenced(map: SortedMap) { - super.beforeDisplaySequenced(map) - beforeInvokes.forEach { - it(map) - } - } - - override fun onDisplay() { - animate.clientOnly() - 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) - } - } - } +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 index f6b04a4f..b1626208 100644 --- 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 @@ -12,9 +12,8 @@ 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.network.PacketByteBuf - +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.player.Player import java.lang.ref.ReferenceQueue @@ -25,34 +24,16 @@ import kotlin.collections.set import kotlin.jvm.java object ParticleCompositionManager { - /** - * 弱引用保存已进入客户端显示生命周期的 Composition 实例。 - * - * 示例:顶层 Composition 及其直接显示的子 Composition 会分别占一个条目。 - * 禁止在此处强持有 Composition,否则 Emitter 临时创建的实例无法被回收。 - */ private val loadedClientCompositions = WeakIdentitySet() val clientView = ConcurrentHashMap() val serverView = ConcurrentHashMap() - /** - * 只有玩家可见才会处理发包 - */ val playerPlayerVisibleSet = ConcurrentHashMap>() - val registeredTypes = ConcurrentHashMap>() - - /** - * 登记或移除一个客户端活动 Composition。 - * - * 示例:Composition 首次 `display()` 时传入 `true`,执行 `clear(true)` 时传入 `false`。 - * 禁止为服务端仅用于同步的 Composition 传入 `true`。 - * - * @param composition 需要更新活动状态的 Composition 实例 - * @param loaded `true` 表示已加载到客户端,`false` 表示已离开客户端生命周期 - */ + val registeredTypes = ConcurrentHashMap>() + internal fun setClientLoaded(composition: ParticleComposition, loaded: Boolean) { if (loaded) { loadedClientCompositions.add(composition) @@ -61,21 +42,11 @@ object ParticleCompositionManager { } } - /** - * 返回客户端当前活动的全部 Composition 实例数量。 - * - * 示例:F3 调试信息用该值统计顶层、嵌套及 Emitter 直接显示的 Composition。 - * 禁止把该值理解为服务端实例数或已注册类型数。 - * - * @return 当前客户端活动 Composition 数量 - */ @JvmStatic fun loadedClientCount(): Int = loadedClientCompositions.size() - /** 返回当前客户端活动的全部 Composition 快照,包含嵌套实例。 */ internal fun debugCompositions(): List = loadedClientCompositions.snapshot() - /** 返回服务端当前活动 Composition 实例数。 */ fun loadedServerCount(): Int = serverView.size fun addClient(composition: ParticleComposition) { @@ -91,7 +62,6 @@ object ParticleCompositionManager { sendCreateOrUpdate(composition) } - fun register(randomInstance: ParticleComposition) { val id = randomInstance::class.java.name val codec = randomInstance.getCodec() @@ -119,7 +89,6 @@ object ParticleCompositionManager { CooParticlesConstants.logger.info("Compositions 注册完成 耗时 ${end - start} ms") } - fun tickClient() { val iterator = clientView.entries.iterator() while (iterator.hasNext()) { @@ -216,7 +185,7 @@ object ParticleCompositionManager { } val registryAccess = CooParticlesAPI.registryAccessOrNull ?: return val type = composition::class.java.name - val buf = PacketByteBuf(Unpooled.buffer(), registryAccess) + val buf = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) val data = try { registeredTypes[type]!!.encode(buf, composition) ByteArray(buf.readableBytes()).also { buf.readBytes(it) } @@ -291,16 +260,7 @@ object ParticleCompositionManager { } } - /** - * 清理当前客户端世界持有的所有 Composition。 - * - * 示例:客户端断线或换世界时调用该方法,将顶层与嵌套实例一并移出活动计数。 - * 禁止用该方法清理服务端 [serverView]。 - */ fun clearClient() { - // 这里必须走 clear(true) 强制销毁 - // remove() 可能被使用者重写成延迟消散的语义 (例如先 status.disable() 等渐隐结束再真正销毁) - // 客户端断连/换世界要求立刻干净 否则 composition 会脱离 clientView 变成孤儿粒子 clientView.values.forEach { it.clear(true) } @@ -308,15 +268,12 @@ object ParticleCompositionManager { loadedClientCompositions.clear() } - fun clearServer() { - // 同 clearClient 不能依赖使用者重写的 remove() serverView.onEach { it.value.clear(true) }.clear() playerPlayerVisibleSet.clear() } - } private class WeakIdentitySet { @@ -351,7 +308,6 @@ private class WeakIdentitySet { fun clear() { references.clear() while (collectedReferences.poll() != null) { - // 清空队列中已经失效的引用。 } } 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 index 20e2b631..271591c8 100644 --- 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 @@ -3,14 +3,12 @@ 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 { @@ -23,4 +21,4 @@ infix fun Double.minRangeTo(max: Double): DoubleRangeData { infix fun Double.maxRangeTo(min: Double): DoubleRangeData { return DoubleRangeData(min, this) -} \ No newline at end of file +} 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 index acb79b08..09c0d1e3 100644 --- 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 @@ -16,4 +16,4 @@ infix fun Float.minRangeTo(max: Float): FloatRangeData { infix fun Float.maxRangeTo(min: Float): FloatRangeData { return FloatRangeData(min, this) -} \ No newline at end of file +} 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 index 2fb0b504..3ffeaa4e 100644 --- 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 @@ -3,14 +3,12 @@ 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 } @@ -21,4 +19,4 @@ infix fun Int.minRangeTo(max: Int): IntRangeData { infix fun Int.maxRangeTo(min: Int): IntRangeData { return IntRangeData(min, this) -} \ No newline at end of file +} 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 index e342b4b6..e5285e9c 100644 --- 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 @@ -1,7 +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" } } -} \ No newline at end of file +} 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 index 1a8b4a4c..f50c843f 100644 --- 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 @@ -3,27 +3,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.network.PacketByteBuf - +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 -/** - * 自动生成网络 codec 并以实现类完整类名注册的 [ClassEmitters]。 - * - * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; - * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, - * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 - * - * @param pos 发射器生成位置 - * @param world 发射器生效世界 - */ abstract class AutoEmitters(pos: Vec3, world: Level?) : ClassEmitters(pos, world) { override fun getEmittersID(): String { return this::class.java.name } - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + 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 index a65b1c19..face5664 100644 --- 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 @@ -3,30 +3,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.network.PacketByteBuf - +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 -/** - * 自动生成网络 codec 并以实现类完整类名注册的 [ClassParticleEmitters]。 - * - * 实现类需标注 [CooAutoRegister]。普通同步字段可以使用 [CodecField]; - * 会在运行时修改的 Kotlin 属性建议使用 `var value by dirty(initialValue)`, - * 赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 - * - * @see ParticleEmittersRegistryHelper - * @constructor 实现类必须提供空构造方法或 `(Vec3, Level?)` 构造方法 - * - * @param pos 发射器生成位置 - * @param world 发射器生效世界 - */ abstract class AutoParticleEmitters(pos: Vec3, world: Level?) : ClassParticleEmitters(pos, world) { override fun getEmittersID(): String { return this::class.java.name } - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + 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 index ea7b55e4..8932d8e4 100644 --- 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 @@ -3,27 +3,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.network.PacketByteBuf - +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 -/** - * 自动生成网络 codec 并以实现类完整类名注册的 [TransformableCParticleEmitter]。 - * - * 实现类必须标注 [CooAutoRegister],并提供公开的 `(Vec3, Level?)` 构造器。 - * 普通同步字段可以使用 [CodecField];会在运行时修改的 Kotlin 属性建议使用 - * `var value by dirty(initialValue)`,赋值变化时会自动调用 [markDirty],不需要再添加 [CodecField]。 - * 基类生命周期和变换字段会自动编码。 - * - * @param pos 发射器世界坐标 - * @param world 发射器所在世界 - */ abstract class AutoTransformableCParticleEmitter(pos: Vec3, world: Level?) : TransformableCParticleEmitter(pos, world) { final override fun getEmittersID(): String = this::class.java.name - final override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + final override fun getCodec(): ForgeStreamCodec { return ParticleEmittersRegistryHelper.generateCodec(this) } } diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt deleted file mode 100644 index 01992fcc..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/CParticleEmitterSpace.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -/** - * 定义 [TransformableCParticleEmitter] 新生成粒子使用的坐标空间。 - * - * [LOCAL] 把粒子位置和速度保存在发射器局部空间中。粒子生成后仍会跟随发射器平移、 - * [TransformableCParticleEmitter.emitterRotation] 和缩放, - * 也是默认模式。[WORLD] 使用普通 GPU 发射器的世界坐标语义,已经生成的粒子不再受发射器变换影响。 - * 模式切换只影响后续生成的粒子,已有粒子继续留在各自创建时的空间中。 - * - * @property networkId 稳定的网络编号,不依赖枚举声明顺序 - */ -enum class CParticleEmitterSpace(val networkId: Int) { - /** 粒子属于发射器局部空间,并持续跟随发射器变换。 */ - LOCAL(0), - - /** 粒子生成后保留在世界空间,不跟随发射器变换。 */ - WORLD(1); - - companion object { - /** - * 按稳定网络编号恢复发射空间。 - * - * @param networkId 网络数据中的空间编号 - * @return 对应的发射空间 - * @throws IllegalArgumentException 编号未知时抛出 - */ - @JvmStatic - fun fromNetworkId(networkId: Int): CParticleEmitterSpace { - return entries.firstOrNull { it.networkId == networkId } - ?: throw IllegalArgumentException("unknown CParticle emitter space: $networkId") - } - } -} 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 index fde910eb..8bc94c29 100644 --- 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 @@ -21,7 +21,7 @@ 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.network.FriendlyByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.BlockHitResult import net.minecraft.world.phys.Vec3 @@ -33,12 +33,6 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.max -/** - * 通用发射器实现,兼容多种可控对象: - * - [ControlableParticleData] - * - [ParticleComposition] - * - [DisplayEntity] - */ abstract class ClassEmitters( pos: Vec3, override var world: Level?, @@ -55,18 +49,8 @@ abstract class ClassEmitters( var gravity: Double = 0.0 val handlerList = ConcurrentHashMap>() - /** - * 是否启用线段插值器。 - * - * 如果设置为 `true`,每次发射会沿插值结果逐点生成;设置为 `false` 时,每 tick 只执行一次生成。 - */ var enableInterpolator = false - /** - * 发射器位移插值器。 - * - * 仅在 [enableInterpolator] 为 `true` 时生效,可通过 `setRefiner` 调整细分密度。 - */ var emittersInterpolator: Interpolator = LineEmitterInterpolator() .setRefiner(5.0) @@ -103,7 +87,7 @@ abstract class ClassEmitters( } companion object { - fun encodeBase(data: ClassEmitters, buf: PacketByteBuf) { + fun encodeBase(data: ClassEmitters, buf: FriendlyByteBuf) { val handles = data.collectEventHandles() buf.writeInt(handles.size) handles.forEach { @@ -125,8 +109,7 @@ abstract class ClassEmitters( data.wind.getCodec().encode(buf, data.wind) } - /** 写法:先在 codec 的 decode 中创建对象,再把 `buf` 和 `container` 传入此方法,最后继续解码子类字段。 */ - fun decodeBase(container: ClassEmitters, buf: PacketByteBuf) { + fun decodeBase(container: ClassEmitters, buf: FriendlyByteBuf) { val handlerCount = buf.readInt() val handlers = ArrayList() repeat(handlerCount) { @@ -168,12 +151,10 @@ abstract class ClassEmitters( } } - /** 风力方向。 */ var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { it.loadEmitters(this) } - /** 质量,单位 g。 */ var mass: Double = 1.0 override fun start() { @@ -273,39 +254,12 @@ abstract class ClassEmitters( return player.position().distanceTo(spawnPos) <= visibleRange } - /** - * 服务器和客户端都会执行此方法。 - * - * 如需仅在服务端执行,请自行判断: - * ```kotlin - * if (!world!!.isClientSide) { ... } - * ``` - */ abstract fun doTick() - /** - * 可控对象生成器。 - * - * @param lerpProgress 发射器位移插值器的插值进度 - */ abstract fun genControls(lerpProgress: Float): List> - /** - * 在一次生成前会执行。 - * - * @param current 当前插值点的位置 - * @param lerpProgress 当前生成进度 - */ protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - /** - * 通用可控对象的生成后回调。 - * - * @param data 当前生成对象的数据 - * @param spawnPos 生成位置(对象创建后再改该值无效) - * @param particleLerpProgress 当前对象在本次批量生成中的进度 - * @param posLerpProgress 发射器位移插值进度(未启用插值时恒为 1) - */ abstract fun singleControlableAction( controler: Controlable<*>, data: SerializableData, @@ -315,9 +269,6 @@ abstract class ClassEmitters( posLerpProgress: Float, ) - /** - * 处理单个 controler 的移动方案 - */ protected open fun moveSingleControler( controler: Controlable<*>, data: SerializableData, @@ -327,14 +278,6 @@ abstract class ClassEmitters( controler.teleportTo(to) } - /** - * 读取当前 controler 世界坐标。 - * - * 子类可重写以支持自定义可控对象,默认支持: - * - [ControlableParticle] - * - [ParticleComposition] - * - [DisplayEntity] - */ protected open fun resolveControlerPos( controler: Controlable<*>, data: SerializableData @@ -348,11 +291,6 @@ abstract class ClassEmitters( } } - /** - * 读取当前 controler 的速度向量。 - * - * 子类可重写以支持自定义数据类型;默认对 [ControlableParticleData] 生效。 - */ protected open fun resolveControlerVelocity( controler: Controlable<*>, data: SerializableData @@ -387,12 +325,6 @@ abstract class ClassEmitters( } } - - /** - * 数据同步需要实现此方法。 - * - * @param emitters 需要同步到当前实例的发射器模板 - */ override fun update(emitters: ParticleEmitters) { if (emitters !is ClassEmitters) return this.posState.setCodecValue(emitters.pos) @@ -407,5 +339,4 @@ abstract class ClassEmitters( 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 index 05c67996..d815def9 100644 --- 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 @@ -24,7 +24,7 @@ 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.network.FriendlyByteBuf import net.minecraft.world.entity.Entity import net.minecraft.world.level.Level import net.minecraft.world.phys.BlockHitResult @@ -35,7 +35,6 @@ import java.util.concurrent.ConcurrentHashMap import kotlin.math.max import kotlin.math.pow -/** 通过自定义类来实现一些发散性粒子样式 (实在懒得写表达式了) */ abstract class ClassParticleEmitters( pos: Vec3, override var world: Level?, @@ -55,24 +54,13 @@ abstract class ClassParticleEmitters( private set val handlerList = ConcurrentHashMap>() - /** - * 是否启用线段插值器 如果设置为true 则spawnParticles由线段插值器管控 设置为false,则只会在 - * 1tick内执行一次spawnParticles - */ var enableInterpolator = false - /** - * 插值器工具 需要启用 enableInterpolator 可以修改 - * - * @see enableInterpolator - */ var emittersInterpolator: Interpolator = LineEmitterInterpolator() .setRefiner(5.0) override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { val handlerID = handler.getHandlerID() - // 自动注册不适用于多人 - // TODO if (!ParticleEventHandlerManager.hasRegister(handlerID)) { ParticleEventHandlerManager.register(handler) } @@ -99,16 +87,14 @@ abstract class ClassParticleEmitters( private fun collectEventHandles(): List { return handlerList.flatMap { - // 保留innerClass为false的 it.value.filter { it -> !it.value }.keys } } - companion object { - fun encodeBase(data: ClassParticleEmitters, buf: PacketByteBuf) { + fun encodeBase(data: ClassParticleEmitters, buf: FriendlyByteBuf) { val handles = data.collectEventHandles() buf.writeInt(handles.size) handles.forEach { @@ -131,8 +117,7 @@ abstract class ClassParticleEmitters( data.wind.getCodec().encode(buf, data.wind) } - /** 写法 先在codec的 decode方法中 创建此对象 然后将buf和container 传入此方法 然后继续decode自己的参数 */ - fun decodeBase(container: ClassParticleEmitters, buf: PacketByteBuf) { + fun decodeBase(container: ClassParticleEmitters, buf: FriendlyByteBuf) { val handlerCount = buf.readInt() val handlerList = ArrayList() repeat(handlerCount) { @@ -171,51 +156,22 @@ abstract class ClassParticleEmitters( this.wind = wind this.enableInterpolator = enableInterpolator this.emittersInterpolator.setRefiner(interpolatorCount) - } - } - } - /** 风力方向 */ var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { it.loadEmitters(this) } - /** - * 旧版 GPU 力场列表接口。 - * - * 旧 emitter 可以继续覆写此方法;桥接层会在每 tick 调用 - * [submitCParticleForces],由默认实现把列表内容提交到 sink。 - * 新 emitter 应直接覆写流式接口,避免创建临时列表。 - * - * @return 按执行顺序排列的力场列表 - */ open fun cparticleForces(): List = emptyList() - /** - * GPU 模式附加力场 (每 tick 同步一次)。 - * - * 默认实现兼容旧版 [cparticleForces];新 emitter 可以直接向 sink 提交, - * 从而避免每 tick 构建临时列表。内置 ParticleCommand 可用 - * [CParticleForce.fromCommand] 转换。 - * - * @param sink 当前 emitter 的力场快照,调用期间只应提交本 emitter 的力 - */ open fun submitCParticleForces(sink: CParticleForceSink) { sink.submitAll(cparticleForces()) } - /** - * CParticle 方块碰撞相对 system 原点的最大保证范围,单位为方块。 - * - * 示例:粒子最远会离开 system 原点 40 格时返回 `40`。 - * 禁止:返回值会被限制在 `0..96`;范围越大,网格刷新成本按立方增长。 - */ open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE - /** 质量 单位 g */ var mass: Double = 1.0 override fun start() { if (playing) return @@ -250,16 +206,13 @@ abstract class ClassParticleEmitters( 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) // 用于设置其他插值 + doSubtick(pos, lerpProgress) spawnParticle(pos, lerpProgress) } } else { @@ -280,7 +233,6 @@ abstract class ClassParticleEmitters( return } val world = world as ClientLevel - // 生成粒子样式 var spawnedCount = 0F val particles = genParticles(lerpProgress) val total = particles.size @@ -298,39 +250,12 @@ abstract class ClassParticleEmitters( } } - /** - * 服务器和客户端都会执行此方法 判断服务器清使用 - * - * ```kotlin - * if(!world!!.isClient) - * ``` - */ abstract fun doTick() - /** - * 粒子样式生成器 - * @param lerpProgress 粒子发射器位移插值器的插值进度 - * */ abstract fun genParticles(lerpProgress: Float): List> - /** - * 在一次粒子生成前会执行 - * - * @param current 当前插值的生成位置 - * @param lerpProgress 粒子生成的进度 - */ protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - /** - * 如若要修改粒子的位置, 速度 属性 请直接修改 ControlableParticleData - * - * @param data 用于操作单个粒子属性的类 - * @param spawnPos 生成的位置,在粒子已经生成后再修改无效 - * 执行tick方法请使用controler.addPreTickAction - * @param particleLerpProgress - * 生成这个粒子的时候,当前的进度((当前生成的粒子索引+1)/genParticles(lerpProgress: Float).size ) - * @param posLerpProgress 当发射器进行发射插值时, 插值的偏移 如果不使用插值则永远为1 - */ abstract fun singleParticleAction( controler: ParticleControler, data: ControlableParticleData, @@ -340,15 +265,6 @@ abstract class ClassParticleEmitters( posLerpProgress: Float, ) - /** - * 当粒子死亡(age >= maxAge然后remove)时 会执行这个方法 - * - * @param oldControler - * @param oldData - * @param respawnCount 重新生成的次数 (0代表第一次从emitter生成) - * @param reason 粒子移除原因 - * @return 会根据data生成新粒子 第二个pair是和粒子死亡位置的相对位置(不是发射器相对位置) - */ open fun singleParticleDeathAction( oldControler: ParticleControler, oldData: ControlableParticleData, @@ -366,12 +282,10 @@ abstract class ClassParticleEmitters( posLerpProgress: Float, cparticleBatchSize: Int, ) { - val player = Minecraft.getInstance().player ?: return if (player.position().distanceTo(pos) > data.visibleRange) { return } - // cparticle GPU 路径: 数据直接进 GPU 粒子系统, 跳过 controler/事件/碰撞 if (data is ControlableCParticleData && CParticleEmitterBridge.trySpawn(this, world, pos, data, cparticleBatchSize) ) { @@ -381,12 +295,9 @@ abstract class ClassParticleEmitters( effect.controlUUID = data.uuid val displayer = data.getDisplayer() val control = data.createControler(world, pos, particleLerpProgress, posLerpProgress) as ParticleControler - // 事件层 control.addPreTickAction { - // 针对 ParticleHitEntityEvent 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 @@ -404,24 +315,16 @@ abstract class ClassParticleEmitters( } control.addPreTickAction { - // 针对 ParticleOnLiquidEvent 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 - // 判断现在的位置是不是液体 - if (!world.shouldTickBlocksAt(blockPos)) { - return@addPreTickAction - } val state = world.getBlockState(blockPos) val currentLiquid = !state.isSolid control.bufferedData["cross_liquid"] = currentLiquid if (beforeLiquid || !currentLiquid) { return@addPreTickAction } - // 前一个tick不是液体 当前tick是液体则触发事件 val event = ParticleOnLiquidEvent(this, data, blockPos) for ((handler, _) in hitEntityHandlers) { if (handler.getTargetEventID() != ParticleOnLiquidEvent.EVENT_ID) { @@ -436,8 +339,6 @@ abstract class ClassParticleEmitters( 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, _) -> @@ -474,44 +375,38 @@ abstract class ClassParticleEmitters( 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) { - // 找方向 velocity - handlerList[ParticleOnGroundEvent.EVENT_ID]?.let { - val offset = clipRes.direction.normal.asVec3() * 0.1 - val event = ParticleOnGroundEvent( - this, - data, - ofFloored(prepareMove), - clipRes.location.add(offset), - clipRes - ) - for ((handler, _) in it) { - if (handler.getTargetEventID() != ParticleOnGroundEvent.EVENT_ID) { - continue - } - handler.handle(event) - if (event.canceled) { - break - } + 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) { - handlerList[ParticleCollideEvent.EVENT_ID]?.let { - val event = ParticleCollideEvent( - this, data, clipRes - ) - for ((handler, _) in it) { - if (handler.getTargetEventID() != ParticleCollideEvent.EVENT_ID) { - continue - } - handler.handle(event) - if (event.canceled) { - break - } + 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 } } } @@ -525,7 +420,7 @@ abstract class ClassParticleEmitters( 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 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 @@ -538,7 +433,6 @@ abstract class ClassParticleEmitters( wind.loadEmitters(this) } - val windForce = WindDirections.handleWindForce( wind, pos, airDensity, PhysicConstant.DRAG_COEFFICIENT, PhysicConstant.CROSS_SECTIONAL_AREA, v @@ -551,17 +445,6 @@ abstract class ClassParticleEmitters( data.velocity = v.add(a) } - - /** - * # 处理单个粒子的位移位置 - * - 方便更真实的物理模拟 - * - 用于修改particle受到velocity时的移动 - * - * @param particle 被操作位置的粒子 - * @param data 被操作位置的粒子数据 - * @param to 正常情况下粒子应该移动到的位置(单纯loc+velocity) - * @param collide 粒子的碰撞情况 (如果显示碰撞,则代表to位置存在方块 loc不存在) - */ protected open fun moveSingleParticleWithVelocity( particle: ControlableParticle, data: ControlableParticleData, @@ -571,11 +454,6 @@ abstract class ClassParticleEmitters( particle.teleportTo(to) } - /** - * 数据同步需要实现此方法 - * - * @param emitters 更新的模板发射器 - */ override fun update(emitters: ParticleEmitters) { if (emitters !is ClassParticleEmitters) return this.posState.setCodecValue(emitters.pos) @@ -590,5 +468,4 @@ abstract class ClassParticleEmitters( 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 index 3456adf7..ed098cef 100644 --- 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 @@ -1,117 +1,55 @@ package cn.coostack.cooparticlesapi.network.particle.emitters -import cn.coostack.cooparticlesapi.api.controler.Controlable -import cn.coostack.cooparticlesapi.api.controler.SerializableData import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import cn.coostack.cooparticlesapi.network.particle.composition.manager.ParticleCompositionManager -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import io.netty.buffer.Unpooled -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.network.PacketByteBuf -import net.minecraft.network.PacketByteBuf - -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * SerializableData wrapper for ParticleComposition emitters entry. - */ -class CompositionEmittersData( - var compositionType: String = "", - var compositionData: ByteArray = ByteArray(0) -) : SerializableData { - constructor(composition: ParticleComposition) : this() { - setComposition(composition) - } +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +open class CompositionEmittersData : ControlableParticleData() { companion object { - @JvmStatic - val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, data -> - buf.writeUtf(data.compositionType) - buf.writeInt(data.compositionData.size) - buf.writeBytes(data.compositionData) - }, - { buf -> - val type = buf.readUtf() - val size = buf.readInt() - val bytes = ByteArray(size) - buf.readBytes(bytes) - CompositionEmittersData(type, bytes) + 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 } - ) - - @JvmStatic - fun fromComposition(composition: ParticleComposition): CompositionEmittersData { - val buf = PacketByteBuf(Unpooled.buffer()) - composition.getCodec().encode(buf, composition) - val data = ByteArray(buf.readableBytes()) - buf.readBytes(data) - return CompositionEmittersData(composition::class.java.name, data) - } - } - - private var prepared: ParticleComposition? = null - - private fun decodeComposition(): ParticleComposition { - val codec = ParticleCompositionManager.registeredTypes[compositionType] - ?: throw IllegalStateException("ParticleComposition codec not registered for type: $compositionType") - return codec.decode(PacketByteBuf(Unpooled.wrappedBuffer(compositionData.copyOf()))) - } - - private fun cloneComposition(composition: ParticleComposition): ParticleComposition { - val type = composition::class.java - val ins = runCatching { - type.getDeclaredConstructor(Vec3::class.java, Level::class.java) - .apply { isAccessible = true } - .newInstance(composition.position, composition.world) - }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() - - return ins.apply { update(composition) } + } + ) } - private fun resolveComposition(): ParticleComposition { - return prepared ?: decodeComposition().also { prepared = it } - } + var composition: ParticleComposition? = null - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return PACKET_CODEC + override fun getCodec(): ForgeStreamCodec { + return CODEC } override fun clone(): SerializableData { - val composition = runCatching { cloneComposition(resolveComposition()) }.getOrNull() - return if (composition != null) { - CompositionEmittersData(composition) - } else { - CompositionEmittersData(compositionType, compositionData.copyOf()) + return super.clone().also { + val data = it as CompositionEmittersData + data.composition = composition } } - - override fun createControler( - world: ClientLevel, - pos: Vec3, - particleLerpProcess: Float, - posLerpProcess: Float - ): Controlable<*> { - val composition = resolveComposition() - composition.world = world - composition.setPositionWithoutToggle(pos) - prepared = composition - return composition - } - - override fun getDisplayer(): ParticleDisplayer { - val composition = resolveComposition() - return ParticleDisplayer.withComposition(composition) - } - - fun setComposition(composition: ParticleComposition): CompositionEmittersData { - val clone = cloneComposition(composition) - val data = fromComposition(clone) - compositionType = data.compositionType - compositionData = data.compositionData - prepared = clone - return this - } } 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 index 469e16b3..b08d519f 100644 --- 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 @@ -4,263 +4,57 @@ 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 net.minecraft.network.PacketByteBuf - +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import org.joml.Vector3f -/** - * 单个 CParticle GPU 粒子的完整生成数据。 - * - * 额外纹理蒙版、生命周期曲线、旋转和 GPU 更新方式都属于每份 data,且本类型只让当前 data - * 进入 CParticle 路径。同一次 - * `genParticles()` 可以混合本类型与普通 [ControlableParticleData]:前者走 GPU,后者继续执行 - * `singleParticleAction` 和 [sign] 对应的 CPU 行为。基础纹理始终来自父类 [effect], - * [textureSource] 只控制叠加在基础纹理上的可选蒙版。 - * - * Example: - * ```kotlin - * val block = ControlableCParticleData().apply { - * textureSource = textureOfBlock(state) - * } - * val item = ControlableCParticleData().apply { - * textureSource = textureOfItem(stack) - * } - * ``` - * Forbidden: 不要把一次 emitter 调用中的公共纹理写回 emitter 单例。 - */ open class ControlableCParticleData : ControlableParticleData() { - /** - * # STATIC - * 生成后不保留本 data; - * - * # DYNAMIC - * 允许同步可变外观 - * - * 不建议使用DYNAMIC,因为GPU粒子不支持singleAction, - * 这会导致你必须得额外存储每一个粒子的Data,并且按照对应的方式自己修改 - * 非常浪费性能的同时,也不是这个API希望的做法, - * 颜色、生命周期透明度和缩放都已有 STATIC 曲线,不需要为这类变化使用 DYNAMIC。 - * - * 按照GPU粒子的设定一般使用STATIC - * */ - var updateMode: CParticleUpdateMode = CParticleUpdateMode.STATIC - /** - * 叠加在 [effect] 基础纹理上的可选蒙版;`null` 表示不叠加蒙版。 - * - * Example: 同一 emitter 的不同 CParticle data 可以叠加不同方块或物品纹理。 - * Forbidden: 生成后的 STATIC 粒子不会继续引用或观察此对象。 - */ 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) - /** 按 `age / maxAge` 在 GPU 中采样的不透明度乘数曲线。 */ - var alphaCurve: CParticleCurve? = null - - /** - * 按 `age / maxAge` 同时缩放 X/Y 尺寸的等比 GPU 曲线。 - * - * 最终会与 [scaleXCurve]、[scaleYCurve] 的对应轴倍率相乘。 - * Example: `scaleCurve = CParticleCurve.fadeInOut()` 会保持当前宽高比例。 - * Forbidden: 不要用它单独控制某一个轴。 - */ - var scaleCurve: CParticleCurve? = null - - /** - * 按 `age / maxAge` 只缩放 X 方向尺寸的 GPU 曲线。 - * - * 示例:从 `0.2F` 变化到 `1F` 可以让粒子横向展开。 - * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 - */ - var scaleXCurve: CParticleCurve? = null - - /** - * 按 `age / maxAge` 只缩放 Y 方向尺寸的 GPU 曲线。 - * - * 示例:从 `1F` 变化到 `0F` 可以让粒子纵向收拢。 - * Forbidden: 不要把本字段当成 Z 方向或等比缩放入口。 - */ - var scaleYCurve: CParticleCurve? = null - - /** 按 `age / maxAge` 与基础颜色相乘的 GPU RGB 曲线。 */ - var colorCurve: CParticleColorCurve? = null - - /** ROTATION 模式的指向向量;`null` 时使用父类 yaw/pitch。 */ - var rotationDirection: Vector3f? = null - - /** 欧拉角速度,单位 rad/tick;x=pitch、y=yaw、z=roll。 */ - var angularVelocity: Vector3f = Vector3f() - - /** 每个整数 tick 使用稳定随机种子重新选择纹理动画帧。 */ - var randomAgePreTick: Boolean = false - - /** GPU 随机帧和随机 UV 的种子;`null` 表示生成时自动分配。 */ - var randomSeed: Int? = null - - /** - * 是否使用 CParticle 共享方块占用网格处理本粒子的位移碰撞。 - * - * 碰撞响应与 `moveSingleParticleWithVelocity` 的常用实现一致:命中后停在表面前, - * 并移除速度的法线分量。复杂 `VoxelShape` 会按完整方块近似。 - * 共享缓存覆盖 system 原点附近的 `64³` 方块,分桶边缘至少保留 24 格;缓存外按未命中处理。 - * Example: 同一次 `genParticles()` 可以只给落尘 data 设置为 `true`。 - * Forbidden: 不要用它代替需要台阶、栅栏或实体精确形状的 CPU 碰撞。 - */ - var blockCollision: Boolean = false - - /** 供 Force Command 的 CommandMask 选择。 */ - var commandMask: Int = 0 - - /** metadata 扩展标志位。 */ - var metadataFlags: Int = 0 - - /** 粒子电荷;NaN 表示由 Charge command 使用默认值。 */ - var charge: Float = Float.NaN - - /** 粒子半径,供单点 Lennard-Jones 使用。 */ - var radius: Float = 0F - - /** - * 保存专用 data 的网络编解码器。 - * - * Example: `@CodecField var template = ControlableCParticleData()` 会通过此 codec 同步。 - * Forbidden: 不要用父类 codec 解码本类型,否则 [textureSource] 会丢失。 - */ companion object { - /** - * 先处理继承的基础字段,再处理可空纹理来源。 - * - * Example: 编解码后 `sign`、velocity、effect 和 textureSource 都保持不变。 - * Forbidden: 字段顺序不能与 [decode] 分离修改。 - */ - @JvmField - val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of(::encode, ::decode) - - /** - * 编码基础字段和当前 data 的可选纹理来源。 - * - * Example: `null` 只写入一个 false,客户端继续使用 effect。 - * Forbidden: 不要在这里解析模型或 UV。 - * - * @param buf 目标网络缓冲区 - * @param data 要编码的 CParticle data - */ - private fun encode(buf: PacketByteBuf, data: ControlableCParticleData) { - encodeBase(buf, data) - val source = data.textureSource - buf.writeBoolean(source != null) - if (source != null) { - CParticleTextureSource.STREAM_CODEC.encode(buf, source) - } - buf.writeByte(data.updateMode.ordinal) - writeNullable(buf, data.alphaCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.scaleCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.scaleXCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.scaleYCurve, CParticleCurve.STREAM_CODEC) - writeNullable(buf, data.colorCurve, CParticleColorCurve.STREAM_CODEC) - val direction = data.rotationDirection - buf.writeBoolean(direction != null) - if (direction != null) buf.writeVector3f(direction) - buf.writeVector3f(data.angularVelocity) - buf.writeBoolean(data.randomAgePreTick) - val seed = data.randomSeed - buf.writeBoolean(seed != null) - if (seed != null) buf.writeInt(seed) - buf.writeBoolean(data.blockCollision) - buf.writeInt(data.commandMask) - buf.writeInt(data.metadataFlags) - buf.writeFloat(data.charge) - buf.writeFloat(data.radius) - } - - /** - * 解码基础字段,并在同一个子类实例上恢复纹理来源。 - * - * Example: Item 来源解码后仍持有独立的 ItemStack 快照。 - * Forbidden: 不能先通过父类 codec 解码后再强制转换。 - * - * @param buf 来源网络缓冲区 - * @return 完整的 CParticle data - */ - private fun decode(buf: PacketByteBuf): ControlableCParticleData { - val data = decodeBase(buf, ControlableCParticleData()) - if (buf.readBoolean()) { - data.textureSource = CParticleTextureSource.STREAM_CODEC.decode(buf) + 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 } - val updateModeOrdinal = buf.readUnsignedByte().toInt() - require(updateModeOrdinal < CParticleUpdateMode.entries.size) { - "unknown CParticle update mode: $updateModeOrdinal" - } - data.updateMode = CParticleUpdateMode.entries[updateModeOrdinal] - data.alphaCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.scaleCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.scaleXCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.scaleYCurve = readNullable(buf, CParticleCurve.STREAM_CODEC) - data.colorCurve = readNullable(buf, CParticleColorCurve.STREAM_CODEC) - if (buf.readBoolean()) data.rotationDirection = buf.readVector3f() - data.angularVelocity = buf.readVector3f() - data.randomAgePreTick = buf.readBoolean() - if (buf.readBoolean()) data.randomSeed = buf.readInt() - data.blockCollision = buf.readBoolean() - data.commandMask = buf.readInt() - data.metadataFlags = buf.readInt() - data.charge = buf.readFloat() - data.radius = buf.readFloat() - return data - } - - private fun writeNullable( - buf: PacketByteBuf, - value: T?, - codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec, - ) { - buf.writeBoolean(value != null) - if (value != null) codec.encode(buf, value) - } - - private fun readNullable( - buf: PacketByteBuf, - codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec, - ): T? = if (buf.readBoolean()) codec.decode(buf) else null + ) } - /** - * 返回包含纹理来源的专用 codec。 - * - * Example: emitter 自动编码器可从实例查询此 codec。 - * Forbidden: 返回父类 codec 会截断扩展字段。 - * - * @return [PACKET_CODEC] - */ - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = PACKET_CODEC + override fun getCodec(): ForgeStreamCodec { + return CODEC + } - /** - * 复制基础字段和当前纹理来源,返回独立的 [ControlableCParticleData]。 - * - * Example: emitter 模板可以为每个生成结果调用 `clone()`。 - * Forbidden: 派生类若有额外字段,必须覆盖本方法,不能依赖本实现保留派生类型。 - * - * @return 带有新 UUID 的 CParticle data - */ - override fun clone(): ControlableCParticleData { - return ControlableCParticleData().also { - copyTo(it) - it.updateMode = updateMode - it.textureSource = textureSource - it.alphaCurve = alphaCurve - it.scaleCurve = scaleCurve - it.scaleXCurve = scaleXCurve - it.scaleYCurve = scaleYCurve - it.colorCurve = colorCurve - it.rotationDirection = rotationDirection?.let(::Vector3f) - it.angularVelocity = Vector3f(angularVelocity) - it.randomAgePreTick = randomAgePreTick - it.randomSeed = randomSeed - it.blockCollision = blockCollision - it.commandMask = commandMask - it.metadataFlags = metadataFlags - it.charge = charge - it.radius = radius + 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 index 1fcc517a..302701c6 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -13,10 +14,8 @@ 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.ParticleTypes +import net.minecraft.core.particles.ParticleOptions import net.minecraft.network.PacketByteBuf -import net.minecraft.network.PacketByteBuf - import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.UUID @@ -27,21 +26,12 @@ open class ControlableParticleData : SerializableData { val particleTexturesMapper: MutableMap = mutableMapOf() @JvmStatic - val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( + val PACKET_CODEC: ForgeStreamCodec = + ForgeStreamCodec.of( ::encodeBase, { buf -> decodeBase(buf, ControlableParticleData()) }, ) - /** - * 编码所有基础粒子字段,供父类和扩展 data 共用。 - * - * Example: `ControlableCParticleData` 先调用此方法,再写入自己的纹理来源。 - * Forbidden: 子类不能改变这些字段的顺序,否则旧的基础 data 无法解码。 - * - * @param buf 目标网络缓冲区 - * @param data 要编码的基础粒子数据 - */ internal fun encodeBase(buf: PacketByteBuf, data: ControlableParticleData) { buf.writeUUID(data.uuid) buf.writeVec3(data.velocity) @@ -55,7 +45,7 @@ open class ControlableParticleData : SerializableData { buf.writeInt(data.maxAge) buf.writeUtf(data.textureSheet) - ParticleTypes.STREAM_CODEC.encode(buf, data.effect) + ForgeCodecHelper.particleCodecOf(data.effect).encode(buf, data.effect) buf.writeDouble(data.speed) buf.writeDouble(data.speedLimit) @@ -69,390 +59,99 @@ open class ControlableParticleData : SerializableData { buf.writeFloat(data.depthSize) } - /** - * 从缓冲区读取基础字段并写入指定实例。 - * - * Example: 子类传入自己的新实例,解码后仍保留实际运行时类型。 - * Forbidden: 不要在这里固定构造 `ControlableParticleData`,否则子类字段会丢失。 - * - * @param buf 来源网络缓冲区 - * @param target 接收基础字段的实例 - * @return 赋值完成的 [target] - */ - internal fun decodeBase( - buf: PacketByteBuf, - target: T, - ): T { - val uuid = buf.readUUID() - val velocity = buf.readVec3() - val weightSize = buf.readFloat() - val heightSize = buf.readFloat() - val uniformSize = buf.readBoolean() - val visibleRange = buf.readFloat() - val color = buf.readVector3f() - val alpha = buf.readFloat() - val age = buf.readInt() - val maxAge = buf.readInt() - val textureSheet = buf.readUtf() - val effect = ParticleTypes.STREAM_CODEC.decode(buf) as ControlableParticleEffect - effect.controlUUID = uuid - val speed = buf.readDouble() - val speedLimit = buf.readDouble() - val sign = buf.readInt() - val light = buf.readInt() - val cameraOption = ParticleCameraOption.STREAM_CODEC.decode(buf) - val axis = buf.readVec3() - val yaw = buf.readFloat() - val pitch = buf.readFloat() - val roll = buf.readFloat() - val depthSize = buf.readFloat() - return target.apply { - this.uuid = uuid - this.velocity = velocity - this.color = color - this.alpha = alpha - this.uniformSize = uniformSize - this.weightSize = weightSize - this.heightSize = heightSize - this.depthSize = depthSize - this.visibleRange = visibleRange - this.age = age - this.maxAge = maxAge - this.textureSheet = textureSheet - this.effect = effect - this.speed = speed - this.sign = sign - this.speedLimit = speedLimit - this.light = light - this.cameraOption = cameraOption - this.axis = axis - this.yaw = yaw - this.pitch = pitch - this.roll = roll - } - } - - @JvmStatic - fun registerRenderType(type: ParticleRenderType) { - particleTexturesMapper[type.toString()] = type + 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 } } - - /** - * 粒子生成时会传输的控制UUID - */ - var uuid = UUID.randomUUID() - - /** - * 粒子的移动向量 - * 在粒子发射器中 会不断调用这次的参数 - * - * 此选项会一直赋值给实际粒子 - */ + 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 - /** 旧布尔 API 的兼容桥接:true = BILLBOARD,false = ROTATION。 */ - var faceToCamera: Boolean - get() = cameraOption == ParticleCameraOption.BILLBOARD - set(value) { - cameraOption = ParticleCameraOption.fromFaceToCamera(value) - } - - /** AXIS_BILLBOARD 使用的固定轴方向。 */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 如果faceToCamera为false - * 则此参数代表了粒子水平朝向 - * - * 弧度制 - */ - var yaw = 0.0f - - /** - * 如果faceToCamera为false - * 则此参数代表了粒子垂直朝向 - * - * 弧度制 - * - */ - var pitch = 0.0f - - /** - * 此参数代表了粒子滚动朝向 - * - * 弧度制 - * - */ - var roll = 0.0f - - /** 是否保持宽高等比。 */ - var uniformSize = true - - /** 粒子宽度大小。 */ - private var currentWeightSize = 0.2f - var weightSize: Float - get() = currentWeightSize - set(value) { - currentWeightSize = value - if (uniformSize) { - currentHeightSize = value - } - } - - /** 粒子高度大小。 */ - private var currentHeightSize = 0.2f - var heightSize: Float - get() = currentHeightSize - set(value) { - currentHeightSize = value - if (uniformSize) { - currentWeightSize = value - } - } - - private var currentDepthSize = 0f - var depthSize: Float - get() = currentDepthSize - set(value) { - currentDepthSize = value - } - - /** 旧 size 兼容属性:设置时同时修改宽高。 */ - var size: Float - get() = (weightSize + heightSize) / 2f - set(value) { - currentWeightSize = value - currentHeightSize = value - } - - /** - * 粒子生成时采用的不透明度 - * - */ - var alpha = 1f - - /** - * 粒子生成时设置的age - * - */ - var age = 0 - - /** - * 粒子最大生命周期 - * - */ - var maxAge = 120 - - /** - * 粒子生成时的亮度 - * (修改粒子亮度时请修改该数据) - * - */ - var light = 15 - - /** - * 粒子可见范围 - */ - var visibleRange = 256f - - /** - * 粒子样式 (必须是可控制的粒子) - * - * 此选项只生效一次 - */ - var effect: ControlableParticleEffect = ControlableEndRodEffect(uuid) - - /** - * 一些特殊标识 - * 用于在single 区分不同类型 分工的粒子 - */ - var sign = 0 - - /** - * 粒子移动速度上限 - * 防止不知道什么原因导致粒子移速过高从而导致客户端卡死 - */ - var speedLimit = 32.0 - - /** - * 粒子生成时采用的颜色,后续控制不生效 - * - * 此选项只应用一次 - */ - var color = Vector3f(1f, 1f, 1f) - - /** - * 粒子重生次数 - * 他会在不断的粒子重生中递增 - */ - var respawnCount = 0 - internal set - - // 脑瘫东西设置了客户端专属 - // 粒子渲染方式 只生效一次 - private var textureSheet: String = "PARTICLE_SHEET_TRANSLUCENT" - - /** - * 粒子移动速度 - * 在ClassParticlesEmitters默认不生效 - */ - var speed: Double = 1.0 - fun textureSheetFromString(sheet: String): ParticleRenderType? { - return particleTexturesMapper[sheet] - } - - /** - * 快速旋转 yaw pitch 到目标点 - * - * @param to 目标相对位置 - */ - fun setRotationTo(to: Vector3f) { - val (x, y, z) = Math3DUtil.calculateEulerAnglesToPoint(to) - this.yaw = y - this.pitch = x - } - - /** - * 快速旋转 yaw pitch 到目标点 - * - * @param to 目标相对位置 - */ - fun setRotationTo(to: Vec3) { - setRotationTo(to.toVector3f()) - } - - /** - * 快速旋转 yaw pitch 到目标点 - * - * @param to 目标相对位置 - */ - fun setRotationTo(to: RelativeLocation) { - setRotationTo(to.toVector3f()) - } - - fun setAxisLocation(axis: RelativeLocation) { - this.axis = Vec3(axis.x, axis.y, axis.z) + override fun getCodec(): ForgeStreamCodec { + return PACKET_CODEC } - fun getTextureSheet(): ParticleRenderType { - return textureSheetFromString(textureSheet) ?: let { - CooParticlesConstants.logger.error("can not find textureSheet $textureSheet you need use ControlableParticleData.registerRenderType() to register mapper") - ParticleRenderType.PARTICLE_SHEET_OPAQUE + 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 } } - /** - * 如果你的参数暴露在外面 可能会被服务器调用 - * 则使用这个 - * - * 输入的参数是你的目标的RenderType的toString的内容 - * - * @param value - */ - fun setTextureSheet(value: String) { - this.textureSheet = value - } - - fun setTextureSheet(value: TextureSheetsEnum){ - this.textureSheet = value.name - } - - /** - * 如果你的参数暴露在外面 (可能会被服务器环境调用) - * 那就不要使用这个方法 使用字符串的 - * 因为脑残的设计导致服务器无法访问 ParticleRenderType 类 - * [setTextureSheet(String)] - * - * @param value - */ - fun setTextureSheet(value: ParticleRenderType) { - this.textureSheet = value.toString() - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return PACKET_CODEC - } - override fun createControler( world: ClientLevel, pos: Vec3, particleLerpProcess: Float, posLerpProcess: Float ): Controlable<*> { - val control = ControlParticleManager.createControl(effect.controlUUID) - val data = this - control.applyInitializedAction { - this.uniformSize = data.uniformSize - this.weightSize = data.weightSize - this.heightSize = data.heightSize - this.depthSize = data.depthSize - this.color = data.color - this.currentAge = data.age - this.lifetime = data.maxAge - this.light = data.light - this.textureSheet = data.getTextureSheet() - this.particleAlpha = data.alpha - this.cameraOption = data.cameraOption - this.axis = data.axis - this.previewAxis = data.axis - this.previewWeightSize = data.weightSize - this.previewHeightSize = data.heightSize - this.previewDepthSize = data.depthSize - this.currentPitch = data.pitch - this.currentYaw = data.yaw - this.currentRoll = data.roll - this.previewPitch = data.pitch - this.previewYaw = data.yaw - this.previewRoll = data.roll - } - return control + return ControlParticleManager.createControler(world, pos, this, particleLerpProcess, posLerpProcess) } override fun getDisplayer(): ParticleDisplayer { - return ParticleDisplayer.withSingle(effect) - } - - override fun clone(): ControlableParticleData { - return ControlableParticleData().also(::copyTo) - } - - /** - * 把可克隆的基础字段复制到另一个 data,并为目标生成新 UUID。 - * - * Example: 扩展类可调用 `super.copyTo(target)` 后补充自己的字段。 - * Forbidden: 网络解码不能调用此方法,因为网络中的 UUID 必须原样保留。 - * - * @param target 接收当前基础字段的实例 - */ - protected fun copyTo(target: ControlableParticleData) { - target.uuid = UUID.randomUUID() - target.velocity = velocity - target.uniformSize = uniformSize - target.weightSize = weightSize - target.heightSize = heightSize - target.depthSize = depthSize - target.color = color - target.alpha = alpha - target.visibleRange = visibleRange - target.age = age - target.maxAge = maxAge - target.effect = effect.clone() - target.textureSheet = textureSheet - target.speed = speed - target.sign = sign - target.speedLimit = speedLimit - target.light = light - target.yaw = yaw - target.pitch = pitch - target.roll = roll - target.cameraOption = cameraOption - target.axis = axis + 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 index f0dc42c8..0198ff91 100644 --- 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 @@ -1,144 +1,55 @@ package cn.coostack.cooparticlesapi.network.particle.emitters -import cn.coostack.cooparticlesapi.api.controler.Controlable -import cn.coostack.cooparticlesapi.api.controler.SerializableData -import cn.coostack.cooparticlesapi.CooParticlesAPI -import cn.coostack.cooparticlesapi.display.DisplayEntityManager import cn.coostack.cooparticlesapi.display.DisplayEntity -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import io.netty.buffer.Unpooled -import net.minecraft.core.RegistryAccess -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.network.PacketByteBuf - -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 - -/** - * SerializableData wrapper for DisplayEntity emitters entry. - */ -class DisplayEntityEmittersData( - var entityType: String = "", - var entityData: ByteArray = ByteArray(0), - var visibleRange: Float = 256f -) : SerializableData { - constructor(entity: DisplayEntity) : this() { - setEntity(entity) - } +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +open class DisplayEntityEmittersData : ControlableParticleData() { companion object { - @JvmStatic - val PACKET_CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, data -> - buf.writeUtf(data.entityType) - buf.writeInt(data.entityData.size) - buf.writeBytes(data.entityData) - buf.writeFloat(data.visibleRange) - }, - { buf -> - val type = buf.readUtf() - val size = buf.readInt() - val bytes = ByteArray(size) - buf.readBytes(bytes) - val visibleRange = buf.readFloat() - DisplayEntityEmittersData(type, bytes, visibleRange).also { - it.registryAccess = buf.registryAccess() - } + 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 } - ) - - @JvmStatic - fun fromEntity(entity: DisplayEntity): DisplayEntityEmittersData { - val registryAccess = entity.world?.registryAccess() ?: registryAccessFallback() - val buf = PacketByteBuf(Unpooled.buffer(), registryAccess) - entity.getCodec().encode(buf, entity) - val data = ByteArray(buf.readableBytes()) - buf.readBytes(data) - return DisplayEntityEmittersData(entity::class.java.name, data).also { - it.registryAccess = registryAccess } - } - - private fun registryAccessFallback(): RegistryAccess { - return CooParticlesAPI.registryAccessOrNull - ?: RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY) - } - } - - private var template: DisplayEntity? = null - private var preparedControler: DisplayEntity? = null - private var registryAccess: RegistryAccess? = null - - private fun decodeEntity(registryAccess: RegistryAccess): DisplayEntity { - val codec = DisplayEntityManager.registeredTypes[entityType] - ?: throw IllegalStateException("DisplayEntity codec not registered for type: $entityType") - return codec.decode( - PacketByteBuf(Unpooled.wrappedBuffer(entityData.copyOf()), registryAccess) ) } - private fun cloneEntity(entity: DisplayEntity): DisplayEntity { - val type = entity::class.java - val ins = runCatching { - type.getDeclaredConstructor(Vec3::class.java, Level::class.java) - .apply { isAccessible = true } - .newInstance(entity.pos, entity.world) - }.getOrNull() ?: type.getDeclaredConstructor().apply { isAccessible = true }.newInstance() - - return ins.apply { update(entity) } - } - - private fun resolveEntity(registryAccess: RegistryAccess? = null): DisplayEntity { - return template ?: decodeEntity( - registryAccess ?: this.registryAccess ?: registryAccessFallback() - ).also { template = it } - } - - private fun createEntity(registryAccess: RegistryAccess? = null): DisplayEntity { - return cloneEntity(resolveEntity(registryAccess)) - } + var displayEntity: DisplayEntity? = null - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return PACKET_CODEC + override fun getCodec(): ForgeStreamCodec { + return CODEC } override fun clone(): SerializableData { - val entity = runCatching { cloneEntity(resolveEntity()) }.getOrNull() - return if (entity != null) { - DisplayEntityEmittersData(entity).also { it.visibleRange = visibleRange } - } else { - DisplayEntityEmittersData(entityType, entityData.copyOf(), visibleRange) + return super.clone().also { + val data = it as DisplayEntityEmittersData + data.displayEntity = displayEntity } } - - override fun createControler( - world: ClientLevel, - pos: Vec3, - particleLerpProcess: Float, - posLerpProcess: Float - ): Controlable<*> { - val entity = createEntity(world.registryAccess()) - entity.world = world - entity.pos = pos - preparedControler = entity - return entity - } - - override fun getDisplayer(): ParticleDisplayer { - val entity = preparedControler ?: createEntity().also { preparedControler = it } - preparedControler = null - return ParticleDisplayer.withDisplayEntity(entity) - } - - fun setEntity(entity: DisplayEntity): DisplayEntityEmittersData { - val clone = cloneEntity(entity) - val data = fromEntity(clone) - entityType = data.entityType - entityData = data.entityData - template = clone - preparedControler = null - return this - } } diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt deleted file mode 100644 index 9f8cf37b..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/ParticleDataFactory.kt +++ /dev/null @@ -1,5 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -fun interface ParticleDataFactory { - fun create(): ControlableParticleData -} \ No newline at end of file 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 index 052327c8..cc856462 100644 --- 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 @@ -4,50 +4,26 @@ 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.network.PacketByteBuf - import net.minecraft.server.level.ServerLevel import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 import java.util.UUID - -/** - * 发射单个粒子的 粒子发射器 - * - * 单个粒子通过 ParticleEmitters 修改粒子参数 - * - * -> 不固定数量的ParticleStyleData - * -> 方便输入的 - */ interface ParticleEmitters : ServerControler, NetworkDirtyMarkable { var pos: Vec3 var world: Level? var tick: Int - /** - * 当maxTick == -1时 - * 代表此粒子不会由生命周期控制 - */ var maxTick: Int var delay: Int var uuid: UUID var canceled: Boolean var playing: Boolean - /** - * @param innerClass 是否在类内添加 - * 在类内添加的event handler不会参与codec传输 - */ fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) fun getEmittersID(): String - /** - * 发射粒子 - * 服务器发包 - * 客户端发射 - */ fun start() fun stop() @@ -56,30 +32,14 @@ interface ParticleEmitters : ServerControler, NetworkDirtyMark fun spawnParticle(pos: Vec3, lerpProgress: Float) - /** - * 更新发射器属性状态 - * 服务器发包到客户端 - */ fun update(emitters: ParticleEmitters) - /** - * 注册时传入的CODEC必须和这个完全一致 - * - * 编解码器 - * - * 编码粒子信息, 当前位置 - */ - fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec + fun getCodec(): ForgeStreamCodec override fun getValue(): ParticleEmitters { return this } - /** - * 标记发射器的完整网络状态需要更新。 - * - * Manager 会在当前服务端 tick 结束时合并发送,同一发射器一 tick 内只会更新一次。 - */ override fun markDirty() { if (world?.isClientSide != true) { ParticleEmittersManager.enqueueDirty(this) @@ -118,5 +78,4 @@ interface ParticleEmitters : ServerControler, NetworkDirtyMark 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 index 1e0a0eff..3f1b07ab 100644 --- 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 @@ -14,8 +14,8 @@ 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.network.PacketByteBuf - +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.player.Player import net.minecraft.world.level.Level @@ -25,40 +25,27 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap object ParticleEmittersManager { - // 已经start的 emitters - val emittersCodec = HashMap>() + val emittersCodec = HashMap>() - /** - * 服务器拥有 - */ val serverEmitters = HashMap() internal val visible = ConcurrentHashMap>() private val dirtyEmitters = ConcurrentHashMap.newKeySet() - /** - * 客户端可视 - */ val clientEmitters = ConcurrentHashMap() - /** 返回客户端当前可见 Emitter 数。 */ fun clientEmitterCount(): Int = clientEmitters.size - /** 返回服务端当前 Emitter 数。 */ fun serverEmitterCount(): Int = serverEmitters.size - fun getCodecFromID(id: String): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec? { + fun getCodecFromID(id: String): ForgeStreamCodec? { return emittersCodec[id] } - - /** - * 在客户端执行 - */ @JvmStatic fun register( id: String, - codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec - ): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { + codec: ForgeStreamCodec + ): ForgeStreamCodec { emittersCodec[id] = codec return codec } @@ -70,7 +57,6 @@ object ParticleEmittersManager { register(id, codec) } - @JvmStatic fun addEmitters(emitters: ParticleEmitters) { if (emitters.world == null) return @@ -187,11 +173,6 @@ object ParticleEmittersManager { return set } - /** - * 玩家离开服务器时清理其 Emitter 可见缓存,使重连后重新发送创建包。 - * - * @param player 已断开连接的服务端玩家 - */ fun clearVisibleFor(player: Player) { visible.remove(player.uuid) } @@ -202,7 +183,6 @@ object ParticleEmittersManager { 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) @@ -312,11 +292,10 @@ object ParticleEmittersManager { ) } - private fun encodeEmittersToArray(emitters: ParticleEmitters): ByteArray { val codec = emitters.getCodec() val registryAccess = emitters.world?.registryAccess() ?: CooParticlesAPI.registryAccessOrNull ?: return ByteArray(0) - val buf = PacketByteBuf( + val buf = RegistryFriendlyByteBuf( Unpooled.buffer(), registryAccess ) @@ -379,7 +358,6 @@ object ParticleEmittersManager { ) return } - // 获取instance val instance = clazz.declaredConstructors.find { it.parameterCount == 0 diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt deleted file mode 100644 index 7c9e5981..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/PhysicConstant.kt +++ /dev/null @@ -1,23 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters - -object PhysicConstant { - /** - * 时间单位 tick - */ - const val EARTH_GRAVITY = 0.05 - - /** - * 海平面空气密度 - */ - const val SEA_AIR_DENSITY = 1.225 - - /** - * 粒子物理拖动系数 - */ - const val DRAG_COEFFICIENT = 0.01 - - /** - * 粒子横截面积 - */ - const val CROSS_SECTIONAL_AREA = 0.01 -} \ No newline at end of file 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 index fb24d55d..03fa7ba1 100644 --- 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 @@ -3,28 +3,16 @@ 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 net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import org.joml.Vector3f import kotlin.math.roundToInt import kotlin.random.Random -/** - * 防止老是要手写等 一堆莫名其妙的随机数据而设立的 - * @param maxAge 粒子最大生命周期 - * @param minAge 粒子最小生命周期 - * @param maxCount 粒子最大个数 - * @param minCount 粒子最小个数 - * @param maxSize 粒子最大尺寸 - * @param minSize 粒子最小尺寸 - * @param maxSpeed 粒子最大速度 - * @param minSpeed 粒子最小速度 - * - */ class SimpleRandomParticleData { companion object { - val PACKET_CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of({ buf, it -> + val PACKET_CODEC: ForgeStreamCodec = ForgeStreamCodec.of({ buf, it -> buf.apply { writeInt(it.maxAge) writeInt(it.minAge) @@ -81,21 +69,12 @@ class SimpleRandomParticleData { }) } - /** - * 解决你写粒子随机生命周期的参数 - */ var maxAge = 10 var minAge = 1 - /** - * 解决你写粒子随机个数的参数 - */ var maxCount = 10 var minCount = 1 - /** - * 解决你写粒子随机大小的参数 - */ private var currentMaxSize = 0.3 var maxSize: Double get() = currentMaxSize @@ -123,9 +102,6 @@ class SimpleRandomParticleData { var maxSizeZ = currentMaxSize var minSizeZ = currentMinSize - /** - * 解决你写粒子随机速度的参数 - */ var minSpeed = 0.1 var maxSpeed = 1.0 @@ -142,7 +118,6 @@ class SimpleRandomParticleData { var leftColor = Vector3f(1f, 1f, 1f) var rightColor = Vector3f(1f, 1f, 1f) - fun getRandomParticleMaxAge(): Int = if (maxAge > minAge) { Random.nextInt(minAge, maxAge) } else minAge @@ -266,5 +241,4 @@ class SimpleRandomParticleData { 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 index 8607fd24..9e1a8008 100644 --- 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 @@ -17,7 +17,7 @@ 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.network.FriendlyByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 import org.joml.Quaternionf @@ -25,142 +25,54 @@ import org.joml.Quaternionfc import java.util.UUID import kotlin.math.max -/** - * 只生成 [ControlableCParticleData] 的可变换 GPU 粒子发射器。 - * - * 每个实例拥有独立的 GPU 坐标空间。[space] 为 [CParticleEmitterSpace.LOCAL] 时,已经生成的粒子会随 - * 发射器一起平移、按 [emitterRotation] 旋转并等比缩放;[particleRotation] 只控制新粒子的出生方向。 - * 切换到 [CParticleEmitterSpace.WORLD] 后,新粒子改用世界坐标, - * 行为与 [ClassParticleEmitters] 的 GPU 路径一致。两类粒子使用不同 system,切换模式不会移动旧粒子。 - * - * 本类型直接实现 [ParticleEmitters],不继承 [ClassParticleEmitters]。逐粒子控制器、粒子事件和死亡重生 - * 不属于 GPU system 能力,因此不提供 CPU 回退。 - * - * @param pos 发射器世界坐标 - * @param world 发射器所在世界 - */ abstract class TransformableCParticleEmitter( pos: Vec3, override var world: Level?, ) : ParticleEmitters { - /** 发射器当前的世界坐标;直接修改后最迟在下一次 tick 同步。 */ private val posState = dirty(pos) override var pos by posState - /** 当前生命周期 tick。 */ override var tick: Int = 0 - /** 生命周期上限;`-1` 表示不按 tick 自动结束。 */ override var maxTick: Int = 120 - /** 两次发射之间的 tick 间隔;小于 `1` 时按每 tick 发射处理。 */ override var delay: Int = 0 - /** 跨服务端和客户端识别同一发射器的 UUID。 */ override var uuid: UUID = UUID.randomUUID() - /** 是否已经结束生命周期。 */ override var canceled: Boolean = false - /** 是否已经启动。 */ override var playing: Boolean = false - /** 新粒子使用的坐标空间。切换不会改变已经生成的粒子。 */ - var space: CParticleEmitterSpace = CParticleEmitterSpace.LOCAL - - /** LOCAL 粒子所在的发射器空间旋转;变化时会带动已经生成的粒子。 */ - var emitterRotation: Quaternionf = Quaternionf() - set(value) { - require(value.isFinite && value.lengthSquared() > 1e-12F) { - "rotation must be finite and non-zero" - } - field = Quaternionf(value).normalize() - } - - /** 新生成粒子的出生位置和初速度旋转;不会改变已经生成的粒子。 */ - var particleRotation: Quaternionf = Quaternionf() - set(value) { - require(value.isFinite && value.lengthSquared() > 1e-12F) { - "particle rotation must be finite and non-zero" - } - field = Quaternionf(value).normalize() - } - - /** 发射器局部空间的等比缩放。必须为正数,保证 GPU 模拟可以换算回局部空间。 */ - var scale: Double = 1.0 - set(value) { - require(value.isFinite() && value > 0.0) { "scale must be finite and positive" } - field = value - } + var airDensity = 0.0 + var gravity: Double = 0.0 + var mass: Double = 1.0 - /** 环境空气密度,用于生成 GPU 阻力。 */ - var airDensity: Double = 0.0 + val handlerList = ConcurrentHashMap>() - /** 每 tick 向世界 Y 负方向施加的重力加速度。 */ - var gravity: Double = 0.0 + var enableInterpolator = false - /** 与 [ClassParticleEmitters] 保持一致的质量数据;当前 GPU 力场不会读取它。 */ - var mass: Double = 1.0 + var emittersInterpolator: Interpolator = LineEmitterInterpolator() + .setRefiner(5.0) - /** 风力方向。GPU 路径只支持非 relative 的全局风。 */ - var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { it.loadEmitters(this) } - - /** 是否沿发射器上一 tick 到当前 tick 的路径插值生成。 */ - var enableInterpolator: Boolean = false - - /** 发射路径插值器。 */ - var emittersInterpolator: Interpolator = LineEmitterInterpolator().setRefiner(5.0) - - /** 发射器在当前 tick 中产生的世界坐标位移。 */ - var emitterVelocity: Vec3 = Vec3.ZERO - private set - - private var lastTickPos: Vec3 = pos - private var lastSyncedTransform = TransformSnapshot.capture(this) - - /** - * 旧版 GPU 力场列表接口。 - * - * 该类不继承 [ClassParticleEmitters],因此单独保留旧签名。 - * 默认实现由流式接口转发,旧 emitter 覆写此方法仍能参与 GPU 模拟。 - * - * @return 按执行顺序排列的力场列表 - */ - open fun cparticleForces(): List = emptyList() - - /** - * 返回当前 GPU system 使用的附加力场。 - * - * 位置和方向参数都沿用 [ClassParticleEmitters] 的世界坐标语义。 - * 默认实现兼容旧版 [cparticleForces];新 emitter 可直接提交到 sink。 - * - * @param sink 当前 emitter 的力场快照 - */ - open fun submitCParticleForces(sink: CParticleForceSink) { - sink.submitAll(cparticleForces()) + var wind: WindDirection = GlobalWindDirection(Vec3.ZERO).also { + it.loadEmitters(this) } - /** 返回方块碰撞网格相对当前发射器中心的保证范围。 */ - open fun cparticleBlockCollisionRange(): Int = CParticleSystemManager.DEFAULT_BLOCK_COLLISION_RANGE - - /** - * 拒绝注册逐粒子事件。 - * - * @param handler 待注册事件处理器 - * @param innerClass 是否为实现类内部处理器 - * @throws UnsupportedOperationException 本发射器没有逐粒子 CPU 控制器 - */ - final override fun addEventHandler(handler: ParticleEventHandler, innerClass: Boolean) { - throw UnsupportedOperationException("TransformableCParticleEmitter does not support per-particle events") + 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 - lastTickPos = pos - emitterVelocity = Vec3.ZERO - lastSyncedTransform = TransformSnapshot.capture(this) if (enableInterpolator) { emittersInterpolator.insertPoint(pos) } @@ -172,322 +84,150 @@ abstract class TransformableCParticleEmitter( override fun tick() { if (canceled || !playing) return - val level = world ?: return + world ?: return doTick() - emitterVelocity = pos - lastTickPos - lastTickPos = pos - - if (!level.isClientSide) { - syncServerTransformIfChanged() + if (!world!!.isClientSide) { increaseTick() return } - - TransformableCParticleEmitterBridge.syncSystems(this) if (enableInterpolator) { emittersInterpolator.insertPoint(pos) } if (tick % max(1, delay) == 0) { if (enableInterpolator) { - val refined = emittersInterpolator.getRefinedResult() - val denominator = (refined.size - 1).coerceAtLeast(1).toFloat() - refined.forEachIndexed { index, location -> - val spawnPos = location.toVector() - val lerpProgress = if (refined.size == 1) 1F else index / denominator - doSubtick(spawnPos, lerpProgress) - spawnParticle(spawnPos, lerpProgress) + 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) + spawnParticle(pos, 1f) } } increaseTick() } + private fun increaseTick() { + if (++tick >= maxTick && maxTick != -1) { + stop() + } + } + override fun spawnParticle(pos: Vec3, lerpProgress: Float) { - val level = world as? ClientLevel ?: return - val player = Minecraft.getInstance().player ?: return - val particles = genParticles(lerpProgress) - val batchSize = particles.size - particles.forEach { (data, relative) -> - val worldPosition = TransformableCParticleEmitterBridge.resolveWorldPosition( - this, - pos, - relative, - ) ?: return@forEach - if (player.position().distanceTo(worldPosition) > data.visibleRange) return@forEach - TransformableCParticleEmitterBridge.trySpawn( - this, - level, - pos, - relative, + 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, - batchSize, + RelativeLocation.of(spawnPos), + world, + particleLerpProgress, + lerpProgress ) } } - /** - * 服务器和客户端每 tick 都会调用,用于更新发射器自身状态。 - * - * 实现必须保证两端执行结果可同步,不能在这里直接访问仅客户端存在的渲染对象。 - */ - abstract fun doTick() - - /** - * 生成本次发射的 GPU 粒子及其相对位置。 - * - * LOCAL 模式下相对位置属于发射器坐标空间;WORLD 模式下会直接加到生成世界坐标。 - * - * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` - * @return 本次要生成的 GPU data 和位置 - */ - abstract fun genParticles(lerpProgress: Float): List> - - /** - * 在路径插值中的一次发射前调用。 - * - * @param current 本次插值后的发射世界坐标 - * @param lerpProgress 发射路径中的插值进度,范围为 `0..1` - */ - protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - - /** - * 追加发射器空间旋转。 - * - * LOCAL 模式下,已经生成的粒子位置和速度都会跟随整个空间旋转。 - * - * @param rotation 发射器空间的增量四元数 - */ - fun rotateEmitter(rotation: Quaternionfc) { - emitterRotation = Quaternionf(emitterRotation).mul(rotation) - syncClientTransform() + 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 } - /** 绕发射器局部 Z 轴追加旋转。 */ - fun rotateEmitter(radian: Double) { - rotateEmitter(Quaternionf().rotateLocalZ(radian.toFloat())) - } + abstract fun doTick() - /** - * 追加新粒子的出生旋转。 - * - * 只改变后续新粒子的出生位置和初速度;已经生成的粒子保持生成时的运动方向。 - * 粒子四边形朝向仍只由 `cameraOption` 和粒子自身旋转字段决定。 - * - * @param rotation 新粒子出生坐标系的增量四元数 - */ - fun rotateParticle(rotation: Quaternionfc) { - particleRotation = Quaternionf(particleRotation).mul(rotation) - syncClientTransform() - } + abstract fun genControls(lerpProgress: Float): List> - /** 绕新粒子出生坐标系的局部 Z 轴追加旋转。 */ - fun rotateParticle(radian: Double) { - rotateParticle(Quaternionf().rotateLocalZ(radian.toFloat())) - } + protected open fun doSubtick(current: Vec3, lerpProgress: Float) {} - /** - * 按世界坐标增量移动发射器。 - * - * @param offset 本次增加的世界坐标位移 - */ - fun translate(offset: Vec3) { - teleportTo(pos + offset) - } + abstract fun singleControlableAction( + controler: Controlable<*>, + data: SerializableData, + spawnPos: RelativeLocation, + spawnWorld: Level, + particleLerpProgress: Float, + posLerpProgress: Float, + ) - /** - * 按世界坐标分量增量移动发射器。 - * - * @param x X 轴增量 - * @param y Y 轴增量 - * @param z Z 轴增量 - */ - fun translate(x: Double, y: Double, z: Double) { - translate(Vec3(x, y, z)) + 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) } - /** - * 设置绝对等比缩放。 - * - * @param new 大于零的缩放值 - */ - fun scale(new: Double) { - scale = new - syncClientTransform() + override fun getValue(): ParticleEmitters { + return this } - /** - * 按倍率增量缩放。 - * - * @param factor 大于零的缩放倍率 - */ - fun scaleBy(factor: Double) { - require(factor.isFinite() && factor > 0.0) { "scale factor must be finite and positive" } - scale(scale * factor) + override fun markDirty() { + if (world?.isClientSide != true) { + ParticleEmittersManager.enqueueDirty(this) + } } - override fun teleportTo(to: Vec3) { - pos = to - syncClientTransform() + override fun remove() { + canceled = true } - override fun rotateToPoint(to: RelativeLocation) { - val next = Quaternionf() - Math3DUtil.rotateQuatToPoint(next, to) - emitterRotation = next - syncClientTransform() + override fun spawn(world: Level, pos: Vec3) { + if (world !is ClientLevel) return + this.world = world + this.pos = pos + ParticleEmittersManager.spawnEmitters(this) } - override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { - val next = Quaternionf() - Math3DUtil.rotateQuatToPoint(next, to) - next.rotateLocalZ(radian.toFloat()) - emitterRotation = next - syncClientTransform() + override fun isValid(): Boolean { + return !canceled } override fun rotateAsAxis(radian: Double) { - rotateEmitter(radian) - } - - override fun update(emitters: ParticleEmitters) { - if (emitters !is TransformableCParticleEmitter || getEmittersID() != emitters.getEmittersID()) return - posState.setCodecValue(emitters.pos) - world = world ?: emitters.world - tick = emitters.tick - maxTick = emitters.maxTick - delay = emitters.delay - uuid = emitters.uuid - canceled = emitters.canceled - playing = emitters.playing - space = emitters.space - emitterRotation = Quaternionf(emitters.emitterRotation) - particleRotation = Quaternionf(emitters.particleRotation) - scale = emitters.scale - gravity = emitters.gravity - airDensity = emitters.airDensity - mass = emitters.mass - wind = emitters.wind.also { it.loadEmitters(this) } - enableInterpolator = emitters.enableInterpolator - emittersInterpolator.setRefiner(emitters.emittersInterpolator.refinerCount) - ParticleEmittersRegistryHelper.updateEmitter(this, emitters) - resetRuntimeState(addInterpolatorPoint = false) - syncClientTransform() - } - - /** 结束后停止接收新粒子,并在已有粒子死亡后释放专属 GPU systems。 */ - internal fun finishClientSystems() { - TransformableCParticleEmitterBridge.finishEmitter(uuid) - } - - private fun syncClientTransform() { - if (world?.isClientSide == true) { - TransformableCParticleEmitterBridge.syncSystems(this) - } - } - - private fun syncServerTransformIfChanged() { - val current = TransformSnapshot.capture(this) - if (current == lastSyncedTransform) return - markDirty() - lastSyncedTransform = current } - private fun increaseTick() { - if (++tick >= maxTick && maxTick != -1) { - stop() - } + override fun rotateToPoint(to: RelativeLocation) { } - private fun resetRuntimeState(addInterpolatorPoint: Boolean) { - lastTickPos = pos - emitterVelocity = Vec3.ZERO - lastSyncedTransform = TransformSnapshot.capture(this) - if (addInterpolatorPoint && enableInterpolator) { - emittersInterpolator.insertPoint(pos) - } + override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { } - private data class TransformSnapshot( - val pos: Vec3, - val rotationX: Float, - val rotationY: Float, - val rotationZ: Float, - val rotationW: Float, - val particleRotationX: Float, - val particleRotationY: Float, - val particleRotationZ: Float, - val particleRotationW: Float, - val scale: Double, - val space: CParticleEmitterSpace, - ) { - companion object { - fun capture(emitter: TransformableCParticleEmitter): TransformSnapshot { - val rotation = emitter.emitterRotation - return TransformSnapshot( - emitter.pos, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - emitter.particleRotation.x, - emitter.particleRotation.y, - emitter.particleRotation.z, - emitter.particleRotation.w, - emitter.scale, - emitter.space, - ) - } - } + override fun teleportTo(to: Vec3) { + if (pos == to) return + pos = to } - companion object { - /** 编码所有可变换 GPU emitter 共用的网络字段。 */ - fun encodeBase(data: TransformableCParticleEmitter, buf: PacketByteBuf) { - 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.writeByte(data.space.networkId) - buf.writeQuaternion(data.emitterRotation) - buf.writeQuaternion(data.particleRotation) - buf.writeDouble(data.scale) - 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) - } - - /** 解码所有可变换 GPU emitter 共用的网络字段。 */ - fun decodeBase(container: TransformableCParticleEmitter, buf: PacketByteBuf) { - container.posState.setCodecValue(buf.readVec3()) - container.tick = buf.readInt() - container.maxTick = buf.readInt() - container.delay = buf.readInt() - container.uuid = buf.readUUID() - container.canceled = buf.readBoolean() - container.playing = buf.readBoolean() - container.space = CParticleEmitterSpace.fromNetworkId(buf.readUnsignedByte().toInt()) - container.emitterRotation = buf.readQuaternion() - container.particleRotation = buf.readQuaternion() - container.scale = buf.readDouble() - container.gravity = buf.readDouble() - container.airDensity = buf.readDouble() - container.mass = buf.readDouble() - container.enableInterpolator = buf.readBoolean() - container.emittersInterpolator.setRefiner(buf.readDouble()) - val windId = buf.readUtf() - container.wind = WindDirections.getCodecFromID(windId).decode(buf).also { - it.loadEmitters(container) - } - container.resetRuntimeState(addInterpolatorPoint = true) - } + 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/command/OrbitMode.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt deleted file mode 100644 index 91991286..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/OrbitMode.kt +++ /dev/null @@ -1,30 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class OrbitMode { - /** - * 物理轨道(默认) - * - * - 切向 + 径向纠正 - * - 手感最真实 - * - 需要限幅,否则启动阶段可能不稳定 - */ - PHYSICAL, - - /** - * 弹簧轨道 - * - * - 半径误差像弹簧一样回弹 - * - 非常稳定,不容易飞走 - * - 手感偏“设计感” - */ - SPRING, - - /** - * 吸附轨道 - * - * - 每 tick 直接把粒子拉到目标半径附近 - * - 最稳定,最不物理 - * - 适合魔法阵 / UI / 装饰粒子 - */ - SNAP -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt deleted file mode 100644 index eeee7078..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleAttractionCommand.kt +++ /dev/null @@ -1,128 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.max - -/** - * 吸引/排斥力(指向目标点的加速度) - * - * - strength > 0:吸引(往目标点拉) - * - strength < 0:排斥(从目标点推开) - * - falloff 决定“近强远弱”的衰减方式 - */ -class ParticleAttractionCommand() : ParticleCommand { - - /** 目标点(世界坐标)。 */ - var target: Supplier = Supplier { Vec3.ZERO } - - /** - * 强度(每 tick 的速度增量基准)。 - * - * - 0.1 ~ 0.5:轻微牵引(像磁力) - * - 0.5 ~ 3.0:明显吸附(像黑洞) - * - 3.0+:非常暴力(需要配合 speedLimit / damping) - * - * 负数则变成排斥力。 - */ - var strength: Double = 0.8 - - /** - * 有效范围尺度(>0)。 - * - * - 越大:衰减越慢,远处也能感受到吸力 - * - 越小:只有靠近目标才明显 - */ - var range: Double = 8.0 - - /** - * 衰减幂次(>=1)。 - * - * - 1:衰减较缓 - * - 2:更像“平方反比”趋势 - * - 3:近距离极强,远处几乎无 - */ - var falloffPower: Double = 2.0 - - /** - * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 - * - * - 建议 0.1 ~ 1.0 - */ - var minDistance: Double = 0.25 - - /** - * @param target 目标点(世界坐标) - * @param strength 强度(>0吸引 <0排斥) - * @param range 范围尺度,越大影响越远 - * @param falloffPower 衰减幂次(>=1) - * @param minDistance 最小距离钳制,防止爆炸/NaN - */ - constructor( - target: Supplier = Supplier { Vec3.ZERO }, - strength: Double = 0.8, - range: Double = 8.0, - falloffPower: Double = 2.0, - minDistance: Double = 0.25, - ) : this() { - this.target = target - this.strength = strength - this.range = range - this.falloffPower = falloffPower - this.minDistance = minDistance - } - - /** 目标点(世界坐标)。 */ - fun target(v: Supplier) = apply { target = v } - - /** - * 强度(每 tick 的速度增量基准)。 - * - * - 0.1 ~ 0.5:轻微牵引(像磁力) - * - 0.5 ~ 3.0:明显吸附(像黑洞) - * - 3.0+:非常暴力(需要配合 speedLimit / damping) - * - * 负数则变成排斥力。 - */ - fun strength(v: Double) = apply { strength = v } - - /** - * 有效范围尺度(>0)。 - * - * - 越大:衰减越慢,远处也能感受到吸力 - * - 越小:只有靠近目标才明显 - */ - fun range(v: Double) = apply { range = v } - - /** - * 衰减幂次。 - * - * - 1:衰减较缓 - * - 2:更像“平方反比”趋势 - * - 3:近距离极强,远处几乎无 - */ - fun falloffPower(v: Double) = apply { falloffPower = v } - - /** - * 最小距离钳制(避免距离=0导致 NaN 或爆炸加速度)。 - * - * - 建议 0.1 ~ 1.0 - */ - fun minDistance(v: Double) = apply { minDistance = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val dir = target.get().subtract(pos) - var d = dir.length() - if (d < 1e-9) return - d = max(d, minDistance) - - val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) - val dv = dir.normalize().scale(strength * falloff) - - data.velocity = data.velocity.add(dv) - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt deleted file mode 100644 index 74acb430..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommand.kt +++ /dev/null @@ -1,46 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle - - -fun interface ParticleCommand { - /** - * 粒子“命令/模块”接口:用于在粒子每个 tick 更新前,对粒子数据进行一次可组合的逻辑处理。 - * - * ## 设计目的 - * 你可以把它理解成 **Unity 粒子系统的一个 Module**(例如:旋转力、阻尼、吸引力、漩涡、流场等): - * - 每个 module 都实现一个 `execute(...)` - * - 在粒子 tick 之前按顺序调用这些 module - * - module 之间通过“修改同一个 data”来叠加效果(通常是 `data.velocity`、`data.yaw/pitch/roll` 等) - * - * ## 调用时机(强烈建议) - * 推荐在 `Emitter.singleParticleAction(...)` 中,将命令挂载到 `ParticleControler.addPreTickAction { ... }`: - * - 这样命令会在 **每个 tick** 都执行一次(而不是只在生成时执行一次) - * - 且执行时你能拿到 “实时的粒子实例状态”(`particle.loc`, `particle.onTheGround`, `particle.currentAge` 等) - * - * ## 参数说明 - * @param data - * 可变的粒子控制数据(你自己的控制层数据)。 - * - **常见修改点**:`data.velocity`(加速度/阻尼/流场)、`data.faceToCamera`、`data.yaw/pitch/roll` 等 - * - 注意:`ControlableParticleData` 中有一部分字段在粒子生成后只生效一次(例如颜色/纹理等), - * 因此命令更适合修改“每 tick 生效”的字段(通常是 velocity 与旋转等)。 - * - * @param particle - * 当前 tick 正在更新的粒子实例(客户端真实粒子对象)。 - * - **常用读取**:`particle.loc`(位置)、`particle.onTheGround`(是否落地)、`particle.currentAge`(年龄)等 - * - **注意**:一般不建议在命令里直接 teleport 粒子位置;更推荐通过 `data.velocity` 影响运动, - * 由发射器的 physics/move 逻辑统一处理。 - * - * ## 组合与顺序 - * 多个命令按添加顺序执行: - * - 前面的命令可能会影响后面的命令输入(例如先吸引再阻尼 vs 先阻尼再吸引) - * - 如果你在命令里做了 clamp/limit(例如限速),通常建议放到最后一个命令执行 - * - * ## 编写建议(实践约定) - * - **保持轻量**:命令每 tick 对每个粒子都会执行,避免昂贵计算/分配对象(尤其是大量粒子场景) - * - **不要产生 NaN**:对距离接近 0 的情况要做钳制(例如 minDistance) - * - **可复用**:命令实例最好可以被多个粒子共享(内部不要缓存“单粒子状态”) - */ - fun execute(data: ControlableParticleData, particle: ControlableParticle) -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt deleted file mode 100644 index 9ad00fcb..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleCommandQueue.kt +++ /dev/null @@ -1,120 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle - -/** - * `ParticleCommand` 的顺序队列:用于把多个“粒子模块/预设”组合成一个可复用的执行链。 - * - * ## 设计目的 - * 让你用非常接近 Unity 粒子系统 “模块叠加”的方式组织效果: - * - 每个 module 都是一个 `ParticleCommand` - * - 通过队列把多个 module 组装起来 - * - 每 tick 调一次 `applyVelocity(...)`,队列会按顺序把所有 module 应用到同一个粒子上 - * - * ## 典型使用方式 - * 在 `Emitter.singleParticleAction(...)` 里把队列挂到粒子 controller 的 preTick: - * ```kotlin - * val queue = ParticleCommandQueue() - * .add(AirDragCommand(0.15)) - * .add(AttractionCommand(target = center, strength = 0.8)) - * - * override fun singleParticleAction( - * controler: ParticleControler, - * data: ControlableParticleData, - * spawnPos: RelativeLocation, - * spawnWorld: Level, - * particleLerpProgress: Float, - * posLerpProgress: Float - * ) { - * controler.addPreTickAction { - * queue.applyVelocity(data, this) - * } - * } - * ``` - */ -class ParticleCommandQueue { - val commands = - ArrayDeque Boolean>>() - - @Suppress("UNCHECKED_CAST") - private val alwaysExecutePredicate = - AlwaysExecutePredicate as ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean - - /** - * 对当前粒子执行队列中的所有命令。 - * - * @param data 当前粒子的可变控制数据(被命令叠加修改) - * @param particle 当前 tick 正在更新的粒子实例(用于读取实时状态) - */ - fun applyVelocity(data: ControlableParticleData, particle: ControlableParticle) { - commands.forEach { - if (it.second(it.first, data, particle)) { - it.first.execute(data, particle) - } - } - } - - /** - * 更新队列中指定索引位置的 Command 数据。 - * - * @param T Command 的具体类型 - * @param applier 对该 Command 执行的更新逻辑(直接修改其内部参数) - * @param index Command 在队列中的索引位置 - */ - fun updateWith(index: Int, applier: T.() -> Unit): ParticleCommandQueue = - apply { - if (index !in commands.indices) { - return@apply - } - val command = commands[index].first - runCatching { applier(command as T) } - } - - /** - * 更新队列中所有符合指定类型的 Command。 - * - * @param T 需要被更新的 Command 类型 - * @param applier 对所有匹配 Command 执行的更新逻辑 - */ - inline fun updateWithTypes(applier: T.() -> Unit): ParticleCommandQueue = apply { - commands.map { it.first }.filterIsInstance() - .forEach { - applier(it) - } - } - - /** - * 向队列末尾追加一个命令,并返回自身以支持链式调用。 - * - * @param command 要追加的粒子命令 - * @return this(便于 `.add(...).add(...)` 链式写法) - */ - fun add(command: ParticleCommand): ParticleCommandQueue { - commands.add(command to alwaysExecutePredicate) - return this - } - - /** - * 向队列末尾追加一个命令,并返回自身以支持链式调用。 - * - * @param command 要追加的粒子命令 - * @param predicate 执行这个命令的条件 - * @return this(便于 `.add(...).add(...)` 链式写法) - */ - fun add( - command: ParticleCommand, - predicate: ParticleCommand.(ControlableParticleData, ControlableParticle) -> Boolean - ): ParticleCommandQueue { - commands.add(command to predicate) - return this - } - - private object AlwaysExecutePredicate : Function3 { - override fun invoke( - command: ParticleCommand, - data: ControlableParticleData, - particle: Any? - ): Boolean = true - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt deleted file mode 100644 index e7190ae1..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDistortionCommand.kt +++ /dev/null @@ -1,334 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.abs -import kotlin.math.floor - -class ParticleDistortionCommand() : ParticleCommand { - var center: Supplier = Supplier { Vec3.ZERO } - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - var radius: Double = 3.0 - var radialStrength: Double = 0.35 - var axialStrength: Double = 0.25 - var tangentialStrength: Double = 0.0 - var frequency: Double = 0.25 - var timeScale: Double = 0.1 - var phaseOffset: Double = 0.0 - var followStrength: Double = 0.35 - var maxStep: Double = 0.6 - var baseAxial: Double = 0.0 - var seedOffset: Int = 0 - var useLifeCurve: Boolean = false - - /** - * @param center 扰动中心点(轴线穿过的点,世界坐标)。 - * - 建议与发射器中心一致,这样“环”会被稳定地扭曲而不是整体漂移。 - * - 使用 Supplier 便于跟随移动的实体/点位(每 tick 取一次)。 - * - * @param axis 扰动轴(环所在平面的法向/法线方向)。 - * - 会自动 normalize,零向量会回退到 (0,1,0)。 - * - (0,1,0) 表示环在 XZ 平面;(1,0,0) 表示环在 YZ 平面。 - * - * @param radius 基础半径(未扰动前的目标环半径)。 - * - 设为发射器环半径时,扰动主要表现为“扭曲”而不是整体收缩/扩张。 - * - 过小会让环过紧,过大则偏离发射器形状。 - * - * @param radialStrength 径向扰动强度(沿“半径方向”的噪声幅度)。 - * - 控制“鼓包/凹陷”的程度,值越大环越不规则。 - * - 设为 0 表示不做径向扭曲,只保留其它方向扰动。 - * - * @param axialStrength 轴向扰动强度(沿“轴线方向”的噪声幅度)。 - * - 控制环在法向上的起伏(像波浪上下抖动)。 - * - 设为 0 则保持在同一平面内。 - * - * @param tangentialStrength 切向扰动强度(沿“绕环方向”的噪声幅度)。 - * - 会让粒子沿环方向滑移,产生“扭拧/拉扯”的感觉。 - * - 设为 0 可避免环周向被拉散。 - * - * @param frequency 空间频率(噪声随位置变化的密度)。 - * - 越大越“细碎/抖动”,越小越“平滑/大块”。 - * - 影响扭曲的纹理尺度。 - * - * @param timeScale 时间变化速度(噪声随时间滚动的快慢)。 - * - 0 表示静态扭曲;越大越“流动/抖动”。 - * - 过大可能出现闪烁感。 - * - * @param phaseOffset 相位偏移(用来错开不同发射器/批次的噪声)。 - * - 不改变强度,只改变噪声的“起点”。 - * - * @param followStrength 追随强度(粒子被拉回扭曲环的力度)。 - * - 越大环越“紧”,不易发散;越小越“松”,容易漂离形状。 - * - * @param maxStep 单 tick 最大修正步长(对速度增量做上限)。 - * - 防止拉回过猛导致抖动或穿透。 - * - <=0 表示不限制。 - * - * @param baseAxial 基础轴向偏移(整体沿轴线的固定偏移量)。 - * - 用于把整个环抬高/降低或制作“偏心”的扭曲面。 - * - * @param seedOffset 噪声种子偏移(同一效果的“变体”)。 - * - 相同 seedOffset 会得到相同扭曲纹理;不同值能做多种随机样式。 - * - * @param useLifeCurve 是否按生命周期衰减扰动强度。 - * - true:出生强、死亡弱,更自然。 - * - false:全生命周期强度恒定,更稳定。 - */ - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - radius: Double = 3.0, - radialStrength: Double = 0.35, - axialStrength: Double = 0.25, - tangentialStrength: Double = 0.0, - frequency: Double = 0.25, - timeScale: Double = 0.1, - phaseOffset: Double = 0.0, - followStrength: Double = 0.35, - maxStep: Double = 0.6, - baseAxial: Double = 0.0, - seedOffset: Int = 0, - useLifeCurve: Boolean = false, - ) : this() { - this.center = center - this.axis = axis - this.radius = radius - this.radialStrength = radialStrength - this.axialStrength = axialStrength - this.tangentialStrength = tangentialStrength - this.frequency = frequency - this.timeScale = timeScale - this.phaseOffset = phaseOffset - this.followStrength = followStrength - this.maxStep = maxStep - this.baseAxial = baseAxial - this.seedOffset = seedOffset - this.useLifeCurve = useLifeCurve - } - - /** - * 设置扰动中心点(轴线穿过的点,世界坐标)。 - * - * - 建议与发射器中心一致,保证环形整体不漂移。 - * - Supplier 可用于跟随移动对象,每 tick 自动读取最新位置。 - */ - fun center(v: Supplier) = apply { center = v } - - /** - * 设置扰动轴(环所在平面的法线方向)。 - * - * - 会在计算时 normalize。 - * - (0,1,0) 为 XZ 平面,(1,0,0) 为 YZ 平面。 - */ - fun axis(v: Vec3) = apply { axis = v } - - /** - * 设置基础半径(未扰动前的目标环半径)。 - * - * - 与发射器环半径一致时最稳定。 - * - 过小会让环“收紧”,过大可能偏离发射器形状。 - */ - fun radius(v: Double) = apply { radius = v } - - /** - * 设置径向扰动强度(沿半径方向的扭曲幅度)。 - * - * - 越大越“鼓包/凹陷”,环越不规则。 - * - 设为 0 可保持圆环半径稳定。 - */ - fun radialStrength(v: Double) = apply { radialStrength = v } - - /** - * 设置轴向扰动强度(沿轴线方向的起伏幅度)。 - * - * - 控制环在法向上的上下波动。 - * - 设为 0 则保持在同一平面内。 - */ - fun axialStrength(v: Double) = apply { axialStrength = v } - - /** - * 设置切向扰动强度(沿环方向的滑移幅度)。 - * - * - 让粒子沿环周向“拉扯/扭拧”。 - * - 过大可能导致环被拉散。 - */ - fun tangentialStrength(v: Double) = apply { tangentialStrength = v } - - /** - * 设置空间频率(噪声随位置变化的密度)。 - * - * - 越大越细碎、越抖;越小越平滑、越大块。 - * - 决定扭曲纹理的尺度。 - */ - fun frequency(v: Double) = apply { frequency = v } - - /** - * 设置时间变化速度(噪声滚动速度)。 - * - * - 0 表示静态扭曲。 - * - 越大越活跃,但过大容易“闪烁抖动”。 - */ - fun timeScale(v: Double) = apply { timeScale = v } - - /** - * 设置相位偏移(错开不同发射器/批次的噪声)。 - * - * - 不影响强度,只改变噪声起点。 - * - 用于避免多个环“同步抖动”。 - */ - fun phaseOffset(v: Double) = apply { phaseOffset = v } - - /** - * 设置追随强度(粒子被拉回扭曲环的力度)。 - * - * - 越大越“紧”,不易发散。 - * - 越小越“松”,允许更自由的漂移。 - */ - fun followStrength(v: Double) = apply { followStrength = v } - - /** - * 设置单 tick 最大修正步长(速度增量上限)。 - * - * - 防止拉回过猛导致抖动或穿透。 - * - <=0 表示不限制。 - */ - fun maxStep(v: Double) = apply { maxStep = v } - - /** - * 设置基础轴向偏移(整体沿轴线的固定偏移量)。 - * - * - 可把环整体抬高/降低。 - * - 配合轴向扰动可做“偏心/倾移”的扭曲环。 - */ - fun baseAxial(v: Double) = apply { baseAxial = v } - - /** - * 设置噪声种子偏移(同效果的随机变体)。 - * - * - 相同 seedOffset -> 相同扭曲纹理。 - * - 不同 seedOffset -> 不同随机外观。 - */ - fun seedOffset(v: Int) = apply { seedOffset = v } - - /** - * 设置是否按生命周期衰减扰动强度。 - * - * - true:出生强、死亡弱,更自然。 - * - false:全生命周期恒定,更稳定。 - */ - fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val centerPos = center.get() - val ax = safeNormalize(axis) - val pos = particle.loc - val r = pos.subtract(centerPos) - val axialComp = ax.scale(r.dot(ax)) - var radial = r.subtract(axialComp) - var radialLen = radial.length() - if (radialLen < 1e-9) { - radial = anyPerp(ax) - radialLen = 1.0 - } else { - radial = radial.scale(1.0 / radialLen) - } - - var tangent = ax.cross(radial) - val tLen = tangent.length() - tangent = if (tLen < 1e-9) anyPerp(ax) else tangent.scale(1.0 / tLen) - - val lifeT = if (particle.lifetime > 0) { - particle.currentAge.toDouble() / particle.lifetime.toDouble() - } else { - 0.0 - } - val lifeMul = if (useLifeCurve) (1.0 - lifeT).coerceIn(0.0, 1.0) else 1.0 - - val time = particle.currentAge.toDouble() * timeScale + phaseOffset - val local = pos.subtract(centerPos) - val p = local.scale(frequency).add(time, time * 0.7, time * 1.3) - val seed = particle.controlUUID.hashCode() + seedOffset - - val nr = noise(p, seed + 11) - val na = noise(p, seed + 23) - val nt = noise(p, seed + 37) - - val targetRadius = (radius + nr * radialStrength * lifeMul).coerceAtLeast(0.0) - val targetAxial = baseAxial + na * axialStrength * lifeMul - val targetTangential = nt * tangentialStrength * lifeMul - - val targetPos = centerPos - .add(ax.scale(targetAxial)) - .add(radial.scale(targetRadius)) - .add(tangent.scale(targetTangential)) - - var dv = targetPos.subtract(pos).scale(followStrength) - val dvLen = dv.length() - if (maxStep > 0.0 && dvLen > maxStep) { - dv = dv.scale(maxStep / dvLen) - } - data.velocity = data.velocity.add(dv) - } - - private fun safeNormalize(v: Vec3): Vec3 { - val len = v.length() - return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) - } - - private fun anyPerp(axis: Vec3): Vec3 { - val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) - val perp = axis.cross(ref) - val len = perp.length() - return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) - } - - private fun noise(p: Vec3, seed: Int): Double { - return valueNoise3(p, seed) * 2.0 - 1.0 - } - - private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) - private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t - - private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { - var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 - n = (n xor (n ushr 13)) * 1274126177 - n = n xor (n ushr 16) - return (n and 0x7fffffff).toDouble() / 2147483647.0 - } - - private fun valueNoise3(p: Vec3, seed: Int): Double { - val x0 = floor(p.x).toInt() - val y0 = floor(p.y).toInt() - val z0 = floor(p.z).toInt() - val fx = p.x - x0 - val fy = p.y - y0 - val fz = p.z - z0 - - val u = fade(fx) - val v = fade(fy) - val w = fade(fz) - - fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) - - val n000 = h(0, 0, 0) - val n100 = h(1, 0, 0) - val n010 = h(0, 1, 0) - val n110 = h(1, 1, 0) - val n001 = h(0, 0, 1) - val n101 = h(1, 0, 1) - val n011 = h(0, 1, 1) - val n111 = h(1, 1, 1) - - val nx00 = lerp(n000, n100, u) - val nx10 = lerp(n010, n110, u) - val nx01 = lerp(n001, n101, u) - val nx11 = lerp(n011, n111, u) - - val nxy0 = lerp(nx00, nx10, v) - val nxy1 = lerp(nx01, nx11, v) - - return lerp(nxy0, nxy1, w) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt deleted file mode 100644 index e908304b..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleDragCommand.kt +++ /dev/null @@ -1,99 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 - -/** - * 空气阻尼(Drag) - * - * 每 tick 把速度乘上 (1 - drag),让粒子慢下来、更“黏”。 - */ -class ParticleDragCommand() : ParticleCommand { - /** - * 阻尼强度(>=0)。 - * - * - 0.0:完全不阻尼 - * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) - * - 0.2 ~ 1.0:明显刹车(碎屑、火花) - * - >1.0:非常快停(像在胶水里) - */ - var damping: Double = 0.15 - - /** - * 最小速度阈值(<=0 表示不使用)。 - * - * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 - */ - var minSpeed: Double = 0.0 - - /** - * 额外的线性阻力(每 tick 额外减去 speed * linear) - * - * - 推荐保持 0,除非你想要“低速更粘”的感觉 - */ - var linear: Double = 0.0 - - /** - * @param damping 阻尼强度(>=0),越大速度衰减越快 - * @param minSpeed 低于该速度则直接归零(<=0 不启用) - * @param linear 线性阻力项(建议 0) - */ - constructor( - damping: Double = 0.15, - minSpeed: Double = 0.0, - linear: Double = 0.0, - ) : this() { - this.damping = damping - this.minSpeed = minSpeed - this.linear = linear - } - - /** - * 阻尼强度(>=0)。 - * - * - 0.0:完全不阻尼 - * - 0.05 ~ 0.2:轻微“粘滞感”(漂浮粒子) - * - 0.2 ~ 1.0:明显刹车(碎屑、火花) - * - >1.0:非常快停(像在胶水里) - */ - fun damping(v: Double) = apply { damping = v } - - /** - * 最小速度阈值(<=0 表示不使用)。 - * - * - 用来避免速度无限接近 0 时还在做无意义计算/抖动 - */ - fun minSpeed(v: Double) = apply { minSpeed = v } - - /** - * 额外的线性阻力(每 tick 额外减去 speed * linear) - * - * - 推荐保持 0,除非你想要“低速更粘”的感觉 - */ - fun linear(v: Double) = apply { linear = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - var v = data.velocity - val speed = v.length() - if (minSpeed > 0 && speed <= minSpeed) { - data.velocity = Vec3.ZERO - return - } - - // exp damping - val factor = GraphMathHelper.expDampFactor(damping, 1.0) - v = v.scale(factor) - - // optional linear drag - if (linear > 0.0) { - val s2 = v.length() - if (s2 > 1e-9) { - val shrink = (1.0 - linear).coerceIn(0.0, 1.0) - v = v.scale(shrink) - } - } - data.velocity = v - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt deleted file mode 100644 index b50e6fb9..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleFlowFieldCommand.kt +++ /dev/null @@ -1,119 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import kotlin.math.cos -import kotlin.math.sin - -/** - * 流场:根据粒子位置/时间生成一个“风向量”,并叠加到速度里。 - * - * 这个实现是解析型流场(sin/cos),特点: - * - 计算非常快 - * - 通过 frequency / amplitude / timeScale 能做出很多“丝带/涡流”效果 - */ -class ParticleFlowFieldCommand() : ParticleCommand { - - /** - * 振幅(每 tick 的速度增量幅度) - * - * - 0.01 ~ 0.1:微风(烟雾漂移) - * - 0.1 ~ 0.8:明显流动(能看出纹理) - * - >0.8:很强(容易乱飞,需要阻尼/限速) - */ - var amplitude: Double = 0.15 - - /** - * 空间频率(越大纹理越密,越小越平缓) - * - * - 0.05 ~ 0.2:大尺度流场 - * - 0.2 ~ 1.0:中小尺度卷曲 - */ - var frequency: Double = 0.25 - - /** - * 时间缩放(控制流场随时间变化速度) - * - * - 0:静态流场(像固定风) - * - 0.02 ~ 0.2:缓慢变动 - * - >0.3:快速抖动/波动 - */ - var timeScale: Double = 0.06 - - /** - * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) - */ - var phaseOffset: Double = 0.0 - - /** - * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) - */ - var worldOffset: Vec3 = Vec3.ZERO - - constructor( - amplitude: Double = 0.15, - frequency: Double = 0.25, - timeScale: Double = 0.06, - phaseOffset: Double = 0.0, - worldOffset: Vec3 = Vec3.ZERO, - ) : this() { - this.amplitude = amplitude - this.frequency = frequency - this.timeScale = timeScale - this.phaseOffset = phaseOffset - this.worldOffset = worldOffset - } - - /** - * 振幅(每 tick 的速度增量幅度) - * - * - 0.01 ~ 0.1:微风(烟雾漂移) - * - 0.1 ~ 0.8:明显流动(能看出纹理) - * - >0.8:很强(容易乱飞,需要阻尼/限速) - */ - fun amplitude(v: Double) = apply { amplitude = v } - - /** - * 空间频率(越大纹理越密,越小越平缓) - * - * - 0.05 ~ 0.2:大尺度流场 - * - 0.2 ~ 1.0:中小尺度卷曲 - */ - fun frequency(v: Double) = apply { frequency = v } - - /** - * 时间缩放(控制流场随时间变化速度) - * - * - 0:静态流场(像固定风) - * - 0.02 ~ 0.2:缓慢变动 - * - >0.3:快速抖动/波动 - */ - fun timeScale(v: Double) = apply { timeScale = v } - - /** - * 相位偏移(不同 emitter/不同组用不同 offset 会避免完全同步) - */ - fun phaseOffset(v: Double) = apply { phaseOffset = v } - - /** - * 是否使用世界坐标偏移(让同一效果在不同位置也不完全一致) - */ - fun worldOffset(v: Vec3) = apply { worldOffset = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val p = particle.loc.add(worldOffset) - val t = (particle.currentAge.toDouble() * timeScale) + phaseOffset - - // A cheap "curl-ish" analytic field - val fx = sin((p.y + t) * frequency) + cos((p.z - t) * frequency) - val fy = sin((p.z + t) * frequency) + cos((p.x + t) * frequency) - val fz = sin((p.x - t) * frequency) + cos((p.y - t) * frequency) - - // normalize-ish (avoid sqrt cost, keep stable magnitude) - val scale = 0.5 - val dv = Vec3(fx * scale, fy * scale, fz * scale).scale(amplitude) - - data.velocity = data.velocity.add(dv) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt deleted file mode 100644 index 7b73bd04..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleGravityCommand.kt +++ /dev/null @@ -1,20 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.particles.ControlableParticle - -/** - * 在emitter内部使用updatePhysics 或者使用这个 - * - * @property emitter 对应粒子发射器 - */ -class ParticleGravityCommand(val emitter: ClassParticleEmitters) : ParticleCommand { - override fun execute( - data: ControlableParticleData, - particle: ControlableParticle - ) { - data.velocity = data.velocity.add(0.0, -emitter.gravity, 0.0) - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt deleted file mode 100644 index 7dd6b709..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritMode.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class ParticleInheritMode { - INITIAL, - CURRENT -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt deleted file mode 100644 index 8c80e5e4..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleInheritVelocityCommand.kt +++ /dev/null @@ -1,340 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ClassParticleEmitters -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import org.joml.Quaternionf -import org.joml.Vector3f -import java.util.function.Supplier -import kotlin.random.Random - -/** - * 继承速度命令(Inherit Velocity)。 - * - * 目标: - * - 让粒子继承“发射源速度”或任意自定义速度源 - * - 支持一次性继承(INITIAL)和持续跟随(CURRENT) - * - 支持按轴屏蔽、生命周期权重、空间转换、阻尼跟随 - * - * 常见场景: - * - 武器挥动拖尾:CURRENT + 适度 damping - * - 喷射火花:INITIAL + 较高 multiplier - */ -class ParticleInheritVelocityCommand() : ParticleCommand { - /** - * 速度来源。 - * - * 默认返回 `Vec3.ZERO`,通常你会传入: - * - `Supplier { emitter.emitterVelocity }` - * - 或任意自定义动态速度源 - */ - var source: Supplier = Supplier { Vec3.ZERO } - - /** - * 继承模式。 - * - * - INITIAL:只在首次执行时继承,后续保持(或按 damping 衰减) - * - CURRENT:每 tick 跟随最新来源速度 - */ - var mode: ParticleInheritMode = ParticleInheritMode.INITIAL - - /** - * 继承倍率。 - * - * - 1.0:等于来源速度 - * - <1.0:弱继承 - * - >1.0:强化继承 - */ - var multiplier: Double = 1.0 - - /** - * 轴向掩码(逐轴相乘)。 - * - * 示例: - * - (1,0,1):忽略 Y 轴继承 - * - (0,1,0):只继承竖直方向 - */ - var axisMask: Vec3 = Vec3(1.0, 1.0, 1.0) - - /** - * 生命周期权重曲线。 - * - * 采样值会乘到继承速度上: - * - 1 -> 全量继承 - * - 0 -> 不继承 - */ - var overLifetime: FloatCurve = ConstantFloatCurve(1.0) - - /** - * 阻尼强度(用于平滑/衰减继承速度)。 - * - * - 0:不平滑,立即跟随 - * - >0:指数平滑,值越大变化越柔和 - */ - var damping: Double = 0.0 - - /** - * 继承速度长度上限。 - * - * - <=0:不限制 - * - >0:超过则截断 - */ - var maxContributionSpeed: Double = 0.0 - - /** - * 继承向量所在空间。 - * - * - WORLD:按世界轴解释 - * - LOCAL:按粒子当前旋转解释后再转到世界空间 - */ - var space: ParticleMotionSpace = ParticleMotionSpace.WORLD - - /** - * 是否启用按粒子固定随机缩放。 - */ - var randomizePerParticle: Boolean = false - - /** - * 随机缩放最小值(按轴独立采样)。 - */ - var randomScaleMin: Double = 1.0 - - /** - * 随机缩放最大值(按轴独立采样)。 - */ - var randomScaleMax: Double = 1.0 - - /** - * 随机种子偏移。 - */ - var randomSeedOffset: Int = 0 - - private val stateKey = "inherit_velocity.state.${System.identityHashCode(this)}" - private val randomScaleKey = "inherit_velocity.random_scale.${System.identityHashCode(this)}" - - private data class InheritState( - var initialized: Boolean = false, - var applied: Vec3 = Vec3.ZERO - ) - - private data class AxisRandomScale( - val x: Double, - val y: Double, - val z: Double - ) - - /** - * @param source 速度来源 - * @param mode 继承模式(INITIAL/CURRENT) - * @param multiplier 继承倍率 - * @param axisMask 轴向掩码 - * @param overLifetime 生命周期权重曲线 - * @param damping 平滑阻尼 - * @param maxContributionSpeed 继承速度上限 - * @param space 继承空间(WORLD/LOCAL) - */ - constructor( - source: Supplier, - mode: ParticleInheritMode = ParticleInheritMode.INITIAL, - multiplier: Double = 1.0, - axisMask: Vec3 = Vec3(1.0, 1.0, 1.0), - overLifetime: FloatCurve = ConstantFloatCurve(1.0), - damping: Double = 0.0, - maxContributionSpeed: Double = 0.0, - space: ParticleMotionSpace = ParticleMotionSpace.WORLD - ) : this() { - this.source = source - this.mode = mode - this.multiplier = multiplier - this.axisMask = axisMask - this.overLifetime = overLifetime - this.damping = damping - this.maxContributionSpeed = maxContributionSpeed - this.space = space - } - - /** - * 便捷构造:直接继承指定发射器的每 tick 位移速度。 - */ - constructor(emitter: ClassParticleEmitters) : this( - source = Supplier { emitter.emitterVelocity } - ) - - /** - * 设置速度来源。 - */ - fun source(v: Supplier) = apply { source = v } - - /** - * 设置继承模式。 - * - * - INITIAL:一次继承 - * - CURRENT:持续跟随 - */ - fun mode(v: ParticleInheritMode) = apply { mode = v } - - /** - * 设置继承倍率。 - */ - fun multiplier(v: Double) = apply { multiplier = v } - - /** - * 设置轴向掩码。 - */ - fun axisMask(v: Vec3) = apply { axisMask = v } - - /** - * 设置生命周期权重曲线。 - */ - fun overLifetime(v: FloatCurve) = apply { overLifetime = v } - - /** - * 设置阻尼强度。 - */ - fun damping(v: Double) = apply { damping = v } - - /** - * 设置继承速度上限。 - */ - fun maxContributionSpeed(v: Double) = apply { maxContributionSpeed = v } - - /** - * 设置继承空间(WORLD/LOCAL)。 - */ - fun space(v: ParticleMotionSpace) = apply { space = v } - - /** - * 设置是否启用按粒子固定随机缩放。 - */ - fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } - - /** - * 设置随机缩放区间。 - */ - fun randomScale(min: Double, max: Double) = apply { - randomScaleMin = min - randomScaleMax = max - } - - /** - * 设置随机种子偏移。 - */ - fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } - - /** - * 每 tick 应用继承速度。 - */ - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val state = particle.controler.bufferedData.getOrPut(stateKey) { InheritState() } as InheritState - - val t = normalizedLifetime(particle) - val lifeMultiplier = overLifetime.sample(t) - val randomScale = resolveRandomScale(particle) - - var target = source.get() - target = applyMask(target) - target = scaleAxis(target, randomScale) - target = target.scale(multiplier * lifeMultiplier) - target = transformBySpace(target, space, particle) - target = clampLength(target, maxContributionSpeed) - - target = when (mode) { - ParticleInheritMode.INITIAL -> { - if (!state.initialized) { - state.initialized = true - target - } else if (damping > 0.0) { - state.applied.scale(GraphMathHelper.expDampFactor(damping, 1.0)) - } else { - state.applied - } - } - - ParticleInheritMode.CURRENT -> { - state.initialized = true - if (damping > 0.0) { - val follow = 1.0 - GraphMathHelper.expDampFactor(damping, 1.0) - state.applied.add(target.subtract(state.applied).scale(follow.coerceIn(0.0, 1.0))) - } else { - target - } - } - } - - val delta = target.subtract(state.applied) - if (delta.lengthSqr() > 1e-12) { - data.velocity = data.velocity.add(delta) - } - state.applied = target - } - - private fun applyMask(vec: Vec3): Vec3 { - return Vec3( - vec.x * axisMask.x, - vec.y * axisMask.y, - vec.z * axisMask.z - ) - } - - private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { - return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) - } - - private fun normalizedLifetime(particle: ControlableParticle): Double { - if (particle.lifetime <= 0) return 0.0 - return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) - } - - private fun transformBySpace( - vec: Vec3, - space: ParticleMotionSpace, - particle: ControlableParticle - ): Vec3 { - if (space == ParticleMotionSpace.WORLD) return vec - - val q = Quaternionf().rotateXYZ( - particle.currentPitch, - particle.currentYaw, - particle.currentRoll - ) - val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) - v.rotate(q) - return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) - } - - private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { - if (!randomizePerParticle) { - return AxisRandomScale(1.0, 1.0, 1.0) - } - val cached = particle.controler.bufferedData[randomScaleKey] - if (cached is AxisRandomScale) { - return cached - } - - val minScale = minOf(randomScaleMin, randomScaleMax) - val maxScale = maxOf(randomScaleMin, randomScaleMax) - val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) - val fixed = if (maxScale - minScale <= 1e-9) { - AxisRandomScale(minScale, minScale, minScale) - } else { - AxisRandomScale( - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale) - ) - } - particle.controler.bufferedData[randomScaleKey] = fixed - return fixed - } - - private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { - if (maxLen <= 0.0) return vec - val len = vec.length() - if (len <= 1e-9 || len <= maxLen) return vec - return vec.scale(maxLen / len) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt deleted file mode 100644 index 1fb7be06..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeMotionCommand.kt +++ /dev/null @@ -1,377 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.ConstantFloatCurve -import cn.coostack.cooparticlesapi.network.particle.emitters.command.curve.FloatCurve -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import org.joml.Quaternionf -import org.joml.Vector3f -import kotlin.random.Random - -/** - * 生命周期运动命令(Force over Lifetime + Velocity over Lifetime)。 - * - * 你可以把它理解为 Unity 里两个模块的组合版: - * - Force over Lifetime:每 tick 给 `data.velocity` 增加一段加速度 - * - Velocity over Lifetime:每 tick 按模式塑形速度(叠加/覆盖/乘法) - * - * 执行流程: - * 1. 计算生命周期进度 `t = currentAge / lifetime` - * 2. 采样 Force 曲线并叠加到当前速度 - * 3. 采样 Velocity 曲线并按 `velocityMode` 生效 - * 4. 可选:应用按粒子固定随机缩放(提升群体离散感) - */ -class ParticleLifetimeMotionCommand() : ParticleCommand { - /** - * Force 曲线 X 分量(每 tick 对 velocity.x 的增量)。 - * - * - 正值:向世界/局部 +X 推 - * - 负值:向世界/局部 -X 推 - */ - var forceX: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Force 曲线 Y 分量(每 tick 对 velocity.y 的增量)。 - * - * 常用于: - * - 上升烟雾:前期正值,后期逐渐趋近 0 - * - 下坠火星:前期小正值,后期转负值 - */ - var forceY: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Force 曲线 Z 分量(每 tick 对 velocity.z 的增量)。 - */ - var forceZ: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Velocity 曲线 X 分量。 - * - * 实际如何生效取决于 `velocityMode`: - * - ADD:视作附加速度曲线 - * - OVERRIDE:视作目标速度 - * - MULTIPLY:视作按轴倍率 - */ - var velocityX: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Velocity 曲线 Y 分量。 - */ - var velocityY: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Velocity 曲线 Z 分量。 - */ - var velocityZ: FloatCurve = ConstantFloatCurve(0.0) - - /** - * Force 曲线所在空间。 - * - * - WORLD:曲线向量直接当世界方向 - * - LOCAL:曲线向量会按粒子当前 pitch/yaw/roll 旋转到世界方向 - */ - var forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD - - /** - * Velocity 曲线所在空间。 - * - * - WORLD:速度塑形不随粒子朝向变化 - * - LOCAL:速度塑形会跟随粒子当前朝向 - */ - var velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD - - /** - * Velocity 曲线作用模式。 - * - * - ADD:把曲线速度作为“附加项”并做增量追踪 - * - OVERRIDE:把当前速度拉向曲线速度 - * - MULTIPLY:按轴对当前速度做乘法 - */ - var velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD - - /** - * 是否启用“按粒子固定随机缩放”。 - * - * 开启后每个粒子只在首次执行时采样一次随机倍率, - * 后续 tick 重用同一倍率,保证轨迹连贯。 - */ - var randomizePerParticle: Boolean = false - - /** - * 随机缩放最小值(按轴独立采样)。 - */ - var randomScaleMin: Double = 1.0 - - /** - * 随机缩放最大值(按轴独立采样)。 - */ - var randomScaleMax: Double = 1.0 - - /** - * 随机种子偏移。 - * - * 在相同参数下改变该值,可快速得到另一套离散分布。 - */ - var randomSeedOffset: Int = 0 - - /** - * 限制 Velocity over Lifetime 在单 tick 的最大速度改变量。 - * - * - <= 0:不限制 - * - > 0:超过该值会被裁剪 - * - * 作用:抑制曲线突变导致的速度跳变或爆速。 - */ - var maxVelocityDeltaPerTick: Double = 0.0 - - private val velocityStateKey = "lifetime_motion.velocity_state.${System.identityHashCode(this)}" - private val randomScaleKey = "lifetime_motion.random_scale.${System.identityHashCode(this)}" - - private data class VelocityState(var appliedVelocity: Vec3 = Vec3.ZERO) - private data class AxisRandomScale(val x: Double, val y: Double, val z: Double) - - /** - * @param forceX Force 曲线 X - * @param forceY Force 曲线 Y - * @param forceZ Force 曲线 Z - * @param velocityX Velocity 曲线 X - * @param velocityY Velocity 曲线 Y - * @param velocityZ Velocity 曲线 Z - * @param forceSpace Force 所在空间 - * @param velocitySpace Velocity 所在空间 - * @param velocityMode Velocity 生效模式 - */ - constructor( - forceX: FloatCurve = ConstantFloatCurve(0.0), - forceY: FloatCurve = ConstantFloatCurve(0.0), - forceZ: FloatCurve = ConstantFloatCurve(0.0), - velocityX: FloatCurve = ConstantFloatCurve(0.0), - velocityY: FloatCurve = ConstantFloatCurve(0.0), - velocityZ: FloatCurve = ConstantFloatCurve(0.0), - forceSpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, - velocitySpace: ParticleMotionSpace = ParticleMotionSpace.WORLD, - velocityMode: ParticleLifetimeVelocityMode = ParticleLifetimeVelocityMode.ADD - ) : this() { - this.forceX = forceX - this.forceY = forceY - this.forceZ = forceZ - this.velocityX = velocityX - this.velocityY = velocityY - this.velocityZ = velocityZ - this.forceSpace = forceSpace - this.velocitySpace = velocitySpace - this.velocityMode = velocityMode - } - - /** - * 一次性设置 Force 曲线 XYZ。 - * - * 示例: - * - `x=0,y>0,z=0`:纯上升 - * - `x/z 对称`:横向扩散 - */ - fun forceCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { - forceX = x - forceY = y - forceZ = z - } - - /** - * 一次性设置 Velocity 曲线 XYZ。 - * - * 建议: - * - 做“轨道化速度”用 `OVERRIDE` - * - 做“微调补偿”用 `ADD` - */ - fun velocityCurves(x: FloatCurve, y: FloatCurve, z: FloatCurve) = apply { - velocityX = x - velocityY = y - velocityZ = z - } - - /** - * 设置 Force 曲线空间。 - * - * - WORLD:固定世界方向 - * - LOCAL:跟随粒子朝向 - */ - fun forceSpace(v: ParticleMotionSpace) = apply { forceSpace = v } - - /** - * 设置 Velocity 曲线空间。 - * - * - WORLD:常用于全局风场/轨道 - * - LOCAL:常用于“朝前推进”类效果 - */ - fun velocitySpace(v: ParticleMotionSpace) = apply { velocitySpace = v } - - /** - * 设置 Velocity 曲线作用模式。 - * - * - ADD / OVERRIDE / MULTIPLY - */ - fun velocityMode(v: ParticleLifetimeVelocityMode) = apply { velocityMode = v } - - /** - * 设置是否启用按粒子固定随机缩放。 - */ - fun randomizePerParticle(v: Boolean) = apply { randomizePerParticle = v } - - /** - * 设置随机缩放区间。 - * - * - 当 `min == max` 时等价于固定倍率 - * - 区间越大,离散感越强,但整体可控性会下降 - */ - fun randomScale(min: Double, max: Double) = apply { - randomScaleMin = min - randomScaleMax = max - } - - /** - * 设置随机种子偏移。 - */ - fun randomSeedOffset(v: Int) = apply { randomSeedOffset = v } - - /** - * 设置 Velocity 曲线每 tick 最大速度改变量。 - */ - fun maxVelocityDeltaPerTick(v: Double) = apply { maxVelocityDeltaPerTick = v } - - /** - * 每 tick 应用生命周期运动。 - */ - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val t = normalizedLifetime(particle) - val randomScale = resolveRandomScale(particle) - - val force = transformBySpace( - scaleAxis(sampleForce(t), randomScale), - forceSpace, - particle - ) - if (force.lengthSqr() > 1e-12) { - data.velocity = data.velocity.add(force) - } - - val sampledVelocity = transformBySpace( - scaleAxis(sampleVelocity(t), randomScale), - velocitySpace, - particle - ) - when (velocityMode) { - ParticleLifetimeVelocityMode.ADD -> applyVelocityAdditive(data, particle, sampledVelocity) - ParticleLifetimeVelocityMode.OVERRIDE -> { - particle.controler.bufferedData.remove(velocityStateKey) - var target = sampledVelocity - if (maxVelocityDeltaPerTick > 0.0) { - val delta = clampLength(target.subtract(data.velocity), maxVelocityDeltaPerTick) - target = data.velocity.add(delta) - } - data.velocity = target - } - - ParticleLifetimeVelocityMode.MULTIPLY -> { - particle.controler.bufferedData.remove(velocityStateKey) - val current = data.velocity - var target = Vec3( - current.x * sampledVelocity.x, - current.y * sampledVelocity.y, - current.z * sampledVelocity.z - ) - if (maxVelocityDeltaPerTick > 0.0) { - val delta = clampLength(target.subtract(current), maxVelocityDeltaPerTick) - target = current.add(delta) - } - data.velocity = target - } - } - } - - private fun applyVelocityAdditive( - data: ControlableParticleData, - particle: ControlableParticle, - sampledVelocity: Vec3 - ) { - val state = particle.controler.bufferedData.getOrPut(velocityStateKey) { VelocityState() } as VelocityState - val delta = clampLength(sampledVelocity.subtract(state.appliedVelocity), maxVelocityDeltaPerTick) - if (delta.lengthSqr() > 1e-12) { - data.velocity = data.velocity.add(delta) - } - state.appliedVelocity = state.appliedVelocity.add(delta) - } - - private fun sampleForce(t: Double): Vec3 { - return Vec3( - forceX.sample(t), - forceY.sample(t), - forceZ.sample(t) - ) - } - - private fun sampleVelocity(t: Double): Vec3 { - return Vec3( - velocityX.sample(t), - velocityY.sample(t), - velocityZ.sample(t) - ) - } - - private fun normalizedLifetime(particle: ControlableParticle): Double { - if (particle.lifetime <= 0) return 0.0 - return (particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) - } - - private fun transformBySpace( - vec: Vec3, - space: ParticleMotionSpace, - particle: ControlableParticle - ): Vec3 { - if (space == ParticleMotionSpace.WORLD) return vec - - val q = Quaternionf().rotateXYZ( - particle.currentPitch, - particle.currentYaw, - particle.currentRoll - ) - val v = Vector3f(vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat()) - v.rotate(q) - return Vec3(v.x.toDouble(), v.y.toDouble(), v.z.toDouble()) - } - - private fun scaleAxis(vec: Vec3, scale: AxisRandomScale): Vec3 { - return Vec3(vec.x * scale.x, vec.y * scale.y, vec.z * scale.z) - } - - private fun resolveRandomScale(particle: ControlableParticle): AxisRandomScale { - if (!randomizePerParticle) { - return AxisRandomScale(1.0, 1.0, 1.0) - } - val cached = particle.controler.bufferedData[randomScaleKey] - if (cached is AxisRandomScale) { - return cached - } - val minScale = minOf(randomScaleMin, randomScaleMax) - val maxScale = maxOf(randomScaleMin, randomScaleMax) - val random = Random(particle.controlUUID.hashCode() + randomSeedOffset) - val fixed = if (maxScale - minScale <= 1e-9) { - AxisRandomScale(minScale, minScale, minScale) - } else { - AxisRandomScale( - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale), - random.nextDouble(minScale, maxScale) - ) - } - particle.controler.bufferedData[randomScaleKey] = fixed - return fixed - } - - private fun clampLength(vec: Vec3, maxLen: Double): Vec3 { - if (maxLen <= 0.0) return vec - val len = vec.length() - if (len <= 1e-9 || len <= maxLen) return vec - return vec.scale(maxLen / len) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt deleted file mode 100644 index c903b551..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleLifetimeVelocityMode.kt +++ /dev/null @@ -1,7 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class ParticleLifetimeVelocityMode { - ADD, - OVERRIDE, - MULTIPLY -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt deleted file mode 100644 index 69a4d496..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleMotionSpace.kt +++ /dev/null @@ -1,6 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -enum class ParticleMotionSpace { - WORLD, - LOCAL -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt deleted file mode 100644 index 9486f3fa..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleNoiseCommand.kt +++ /dev/null @@ -1,205 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.extend.multiply -import cn.coostack.cooparticlesapi.extend.times -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import kotlin.math.floor - -/** - * 噪声扰动(Noise / Turbulence) - * - * 作用:在每个 tick 给粒子速度叠加一股“连续的风”,让轨迹更自然、更有生命感, - * 常用于:烟雾/火焰抖动、能量流扰动、爆炸尘埃乱流、魔法粒子“活跃感”。 - * - * @property strength - * - 噪声加速度强度(每 tick 叠加到 velocity 的幅度) - * - 越大:风越“猛”,轨迹更弯、更乱、更炸;过大时会出现抽搐/抖动、偏航严重、粒子群散架。 - * - 越小:扰动更轻微,只是细腻的呼吸感/轻微摆动,整体运动仍主要由初速度决定。 - * - 经验:火焰/烟雾中等,能量微扰偏小,爆炸尘埃可偏大但必须配合 drag 或 clampSpeed。 - * - * @property frequency - * - 空间频率(噪声随“空间位置”变化的密度,决定扰动“颗粒感”) - * - 越大:噪声变化更快、更碎、更“抖”,局部细节更丰富;过大时会像高频抖动/电流乱颤。 - * - 越小:噪声变化更慢、更平滑,形成大尺度的“风带/云团”漂移;过小会显得太温柔、缺细节。 - * - 直觉:frequency 决定“风场纹理”的粗细(大=细碎,小=粗大块)。 - * - * @property speed - * - 时间滚动速度(噪声场随时间流动的快慢) - * - 越大:风场变化更快,扰动更活跃,像湍急的气流;过大时会像噪声在闪烁,产生“乱抖”的观感。 - * - 越小:风场变化更慢,像缓慢流动的空气/云层;过小会显得“静止抖动”,缺少流体感。 - * - 直觉:speed 决定“风在动得有多快”(不是风的力量,力量是 strength)。 - * - * @property affectY - * - 垂直影响比例(对 Y 方向扰动的缩放) - * - 越大:上下翻滚更明显,更“蓬/炸”,适合爆炸烟尘、魔法能量上涌;过大会显得漂浮、失重。 - * - 越小:更贴地/更平,垂直更稳定;常用于烟雾(避免上下乱跳)或贴地尘土。 - * - 常用:烟雾 0.3~0.7;能量上涌可 1.0~1.5;贴地尘土可 0.0~0.3。 - * - * @property clampSpeed - * - 限速(对最终速度做上限,防止速度叠加越来越快) - * - 越大:允许粒子跑得更快,扰动会更“飘逸/发散”;过大时可能被 strength 一直堆出很高速度。 - * - 越小:更“黏”,粒子速度被压住,群体更稳更聚;过小时会把扰动的动态感也一起压扁。 - * - 直觉:clampSpeed 是“安全阀/天花板”,用来保证效果不会失控。 - * - * @property useLifeCurve - * - 是否按生命周期调制强度(把 strength 乘上寿命曲线) - * - true:扰动会随生命周期变化(常见:出生强→死亡弱,或先弱后强),更像真实流体/能量变化。 - * - false:全程扰动强度恒定,更稳定可控,但可能显得“机械”,不够有呼吸感。 - * - 建议:大部分效果都开 true;只有想要“持续稳定噪声”时才关(例如长时间环境粒子)。 - */ -class ParticleNoiseCommand( - private var strength: Double = 0.03, - private var frequency: Double = 0.15, - private var speed: Double = 0.12, - private var affectY: Double = 1.0, - private var clampSpeed: Double = 0.8, - private var useLifeCurve: Boolean = true, -) : ParticleCommand { - constructor() : this(0.03, 0.15, 0.12, 1.0, 0.8, true) - - /** - * 设置噪声加速度强度(每 tick 叠加到 velocity 的幅度)。 - * - * - 越大:扰动更猛,轨迹更弯、更乱,需注意配合限速或阻尼 - * - 越小:扰动更轻微,仅提供细腻的呼吸感 - */ - fun strength(v: Double) = apply { strength = v } - - /** - * 设置空间频率(噪声随空间位置变化的密度)。 - * - * - 越大:扰动更细碎、更抖动 - * - 越小:扰动更平滑,形成大尺度风带 - */ - fun frequency(v: Double) = apply { frequency = v } - - /** - * 设置时间滚动速度(噪声场随时间变化的快慢)。 - * - * - 越大:风场变化更快,更活跃 - * - 越小:风场变化更慢,更沉稳 - */ - fun speed(v: Double) = apply { speed = v } - - /** - * 设置垂直方向(Y 轴)的扰动影响比例。 - * - * - 越大:上下翻滚更明显,更蓬松 - * - 越小:垂直更稳定,更贴地 - */ - fun affectY(v: Double) = apply { affectY = v } - - /** - * 设置速度上限(最终 velocity 的最大长度)。 - * - * - 用于防止噪声扰动持续叠加导致速度失控 - * - 值越大:运动更飘逸、发散 - * - 值越小:粒子群更稳、更聚 - */ - fun clampSpeed(v: Double) = apply { clampSpeed = v } - - /** - * 设置是否按生命周期曲线调制扰动强度。 - * - * - true:扰动随粒子生命周期变化,更自然、更有呼吸感 - * - false:全程恒定扰动,更稳定、可控 - */ - fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } - - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - // 0..1 生命周期进度 - val t = if (particle.lifetime > 0) particle.currentAge.toDouble() / particle.lifetime.toDouble() else 0.0 - - // 取粒子的稳定 seed(一定要稳定) - // 你可以换成 data.seed / spawnIndex / uuid hash - val seed = particle.controlUUID.hashCode() // 如果没有 id,自己做一个 - - // 时间输入:连续滚动 - val time = (particle.currentAge.toDouble()) * speed - val pos = particle.loc - // 噪声采样点:位置 * 频率 + time - val p = (Vec3(pos.x, pos.y, pos.z) * this.frequency) - .add(time, time * 0.7, time * 1.3) - - var amp = strength - if (useLifeCurve) { - // 这里你换成 data 的曲线:例如 data.strengthCurve.sample(t) - // 我给默认:前强后弱(像烟) - val lifeMul = (1.0 - t) - amp *= lifeMul - } - - val n = noiseVec3(p, seed) - val dv = Vec3(n.x, n.y * affectY, n.z).multiply(amp) - - val v0 = data.velocity - var v = v0.add(dv) - - // 限速(很重要) - val sp2 = v.lengthSqr() - val max2 = clampSpeed * clampSpeed - if (sp2 > max2) { - v = v.normalize().multiply(clampSpeed) - } - - data.velocity = v - } - - private fun fade(t: Double): Double = t * t * t * (t * (t * 6 - 15) + 10) - private fun lerp(a: Double, b: Double, t: Double): Double = a + (b - a) * t - - private fun hash3(ix: Int, iy: Int, iz: Int, seed: Int): Double { - // 纯 hash -> 0..1 - var n = ix * 374761393 + iy * 668265263 + iz * 2147483647 + seed * 374761 - n = (n xor (n ushr 13)) * 1274126177 - n = n xor (n ushr 16) - // 转成 0..1 - return ((n and 0x7fffffff).toDouble() / 2147483647.0) - } - - private fun valueNoise3(p: Vec3, seed: Int): Double { - val x0 = floor(p.x).toInt() - val y0 = floor(p.y).toInt() - val z0 = floor(p.z).toInt() - val fx = p.x - x0 - val fy = p.y - y0 - val fz = p.z - z0 - - val u = fade(fx) - val v = fade(fy) - val w = fade(fz) - - fun h(dx: Int, dy: Int, dz: Int) = hash3(x0 + dx, y0 + dy, z0 + dz, seed) - - val n000 = h(0, 0, 0) - val n100 = h(1, 0, 0) - val n010 = h(0, 1, 0) - val n110 = h(1, 1, 0) - val n001 = h(0, 0, 1) - val n101 = h(1, 0, 1) - val n011 = h(0, 1, 1) - val n111 = h(1, 1, 1) - - val nx00 = lerp(n000, n100, u) - val nx10 = lerp(n010, n110, u) - val nx01 = lerp(n001, n101, u) - val nx11 = lerp(n011, n111, u) - - val nxy0 = lerp(nx00, nx10, v) - val nxy1 = lerp(nx01, nx11, v) - - return lerp(nxy0, nxy1, w) // 0..1 - } - - private fun noiseVec3(pos: Vec3, seed: Int): Vec3 { - // 用不同 seed 偏移得到 xyz 三通道 - val nx = valueNoise3(pos, seed + 11) * 2 - 1 - val ny = valueNoise3(pos, seed + 23) * 2 - 1 - val nz = valueNoise3(pos, seed + 37) * 2 - 1 - val v = Vec3(nx, ny, nz) - return if (v.lengthSqr() < 1e-8) Vec3.ZERO else v.normalize() - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt deleted file mode 100644 index 858fef4c..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleOrbitCommand.kt +++ /dev/null @@ -1,177 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.max - -class ParticleOrbitCommand() : ParticleCommand { - /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 目标轨道半径(世界单位) - * - * - 值越小:轨道越紧凑 - * - 值越大:粒子绕行范围越大 - */ - var radius: Double = 3.0 - - /** - * 角速度强度(每 tick 施加的切向速度增量) - * - * - 较小:缓慢绕行 - * - 较大:快速旋转(建议配合限速或阻尼) - */ - var angularSpeed: Double = 0.35 - - /** - * 径向纠正强度(用于将粒子拉回目标半径) - * - * - 值越小:允许轨道半径有较大漂移 - * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 - */ - var radialCorrect: Double = 0.25 - - /** - * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) - */ - var minDistance: Double = 0.2 - - /** - * 轨道模式 - * - * - PHYSICAL:真实物理(默认) - * - SPRING:弹簧收敛(推荐通用) - * - SNAP:强制吸附(最稳定) - */ - var mode: OrbitMode = OrbitMode.PHYSICAL - - /** - * 仅在 PHYSICAL / SPRING 下生效: - * 每 tick 最大径向修正量(防止启动时被拉飞) - */ - var maxRadialStep: Double = 0.5 - - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - radius: Double = 3.0, - angularSpeed: Double = 0.35, - radialCorrect: Double = 0.25, - minDistance: Double = 0.2, - mode: OrbitMode = OrbitMode.PHYSICAL, - ) : this() { - this.center = center - this.axis = axis - this.radius = radius - this.angularSpeed = angularSpeed - this.radialCorrect = radialCorrect - this.minDistance = minDistance - this.mode = mode - } - - /** 轨道中心点(世界坐标),通过 Supplier 提供以支持动态更新 */ - fun center(v: Supplier) = apply { center = v } - - /** 轨道所在平面的法向量(旋转轴),会在计算时进行 normalize */ - fun axis(v: Vec3) = apply { axis = v } - - /** - * 目标轨道半径(世界单位) - * - * - 值越小:轨道越紧凑 - * - 值越大:粒子绕行范围越大 - */ - fun radius(v: Double) = apply { radius = v } - - /** - * 角速度强度(每 tick 施加的切向速度增量) - * - * - 较小:缓慢绕行 - * - 较大:快速旋转(建议配合限速或阻尼) - */ - fun angularSpeed(v: Double) = apply { angularSpeed = v } - - /** - * 径向纠正强度(用于将粒子拉回目标半径) - * - * - 值越小:允许轨道半径有较大漂移 - * - 值越大:轨道更贴近目标半径,但过大可能导致抖动 - */ - fun radialCorrect(v: Double) = apply { radialCorrect = v } - - /** - * 最小距离钳制,用于避免粒子过于接近轨道中心时出现数值异常(NaN) - */ - fun minDistance(v: Double) = apply { minDistance = v } - - /** - * 轨道模式 - * - * - PHYSICAL:真实物理(默认) - * - SPRING:弹簧收敛(推荐通用) - * - SNAP:强制吸附(最稳定) - */ - fun mode(v: OrbitMode) = apply { mode = v } - - /** - * 仅在 PHYSICAL / SPRING 下生效: - * 每 tick 最大径向修正量(防止启动时被拉飞) - */ - fun maxRadialStep(v: Double) = apply { maxRadialStep = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val ax = axis.normalize() - val centerPos = center.get() - - val r0 = pos.subtract(centerPos) - val axialComp = ax.scale(r0.dot(ax)) - var radial = r0.subtract(axialComp) - - var dist = radial.length() - dist = max(dist, minDistance) - - radial = radial.scale(1.0 / dist) - - // 切向方向 - var tangential = ax.cross(radial) - val tLen = tangential.length() - if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) - - val dvTan = tangential.scale(angularSpeed) - val err = dist - radius - - when (mode) { - OrbitMode.PHYSICAL -> { - val raw = -err * radialCorrect - val step = raw.coerceIn(-maxRadialStep, maxRadialStep) - val dvRad = radial.scale(step) - data.velocity = data.velocity.add(dvTan).add(dvRad) - } - - OrbitMode.SPRING -> { - // 二阶感:误差越大,拉力越强,但更温和 - val spring = -err * radialCorrect - val step = spring.coerceIn(-maxRadialStep, maxRadialStep) - val dvRad = radial.scale(step) - data.velocity = data.velocity.add(dvTan).add(dvRad) - } - - OrbitMode.SNAP -> { - // 直接把粒子“吸附”到目标半径 - val targetPos = centerPos - .add(axialComp) - .add(radial.scale(radius)) - - val snapVec = targetPos.subtract(pos) - data.velocity = data.velocity.add(dvTan).add(snapVec.scale(radialCorrect)) - } - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt deleted file mode 100644 index 29e483e5..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleRotationForceCommand.kt +++ /dev/null @@ -1,98 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier - -/** - * 旋转力:让粒子绕某个轴产生切向速度(像被“搅拌”)。 - * - * 典型效果: - * - 火焰上升的旋涡感 - * - 魔法阵环绕 - * - 爆炸烟雾“打旋” - */ -class ParticleRotationForceCommand() : ParticleCommand { - - /** 旋转中心(世界坐标)。 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** - * 旋转轴(会被自动 normalize)。 - * - * - (0,1,0) 绕 Y 轴旋转(最常用) - * - (1,0,0) 绕 X - * - (0,0,1) 绕 Z - */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 旋转强度(每 tick 切向速度增量)。 - * - * - 0.05 ~ 0.3:轻微旋 - * - 0.3 ~ 2.0:明显旋 - * - >2.0:非常暴力(建议配合阻尼/限速) - */ - var strength: Double = 0.35 - - /** - * 距离衰减范围(>0)。 - * - * - 越大:远处仍然能被带动旋转 - * - 越小:只在中心附近打旋 - */ - var range: Double = 8.0 - - /** 衰减幂次(>=1)。 */ - var falloffPower: Double = 2.0 - - /** - * @param center 旋转中心 - * @param axis 旋转轴(自动 normalize) - * @param strength 旋转强度(每 tick 切向速度增量) - * @param range 衰减范围尺度 - * @param falloffPower 衰减幂次(>=1) - */ - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - strength: Double = 0.35, - range: Double = 8.0, - falloffPower: Double = 2.0, - ) : this() { - this.center = center - this.axis = axis - this.strength = strength - this.range = range - this.falloffPower = falloffPower - } - - fun center(v: Supplier) = apply { - center = v - } - fun axis(v: Vec3) = apply { axis = v } - fun strength(v: Double) = apply { strength = v } - fun range(v: Double) = apply { range = v } - fun falloffPower(v: Double) = apply { falloffPower = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val r = pos.subtract(center.get()) - val dist = r.length() - if (dist < 1e-9) return - - val ax = axis.normalize() - // tangential direction: axis x r - var t = ax.cross(r) - val tLen = t.length() - if (tLen < 1e-9) return - t = t.scale(1.0 / tLen) - - val falloff = GraphMathHelper.inversePowerFalloff(dist, range, falloffPower) - val dv = t.scale(strength * falloff) - - data.velocity = data.velocity.add(dv) - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt deleted file mode 100644 index 69a591f2..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleToroidalCirculationCommand.kt +++ /dev/null @@ -1,226 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.extend.plus -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.abs -import kotlin.math.sqrt - -/** - * 环面回流场。 - * - * 在一圈局部区域里制造“翻卷”轨迹,适合蘑菇云帽檐、烟团边缘这类效果。 - * 它会优先“扭转当前速度方向”,而不是把粒子硬拽成一个环, - * 所以在有阻尼/减速时,卷动也会跟着自然变慢。 - * 它改的是粒子速度,不是 billboard 朝向。 - */ -class ParticleToroidalCirculationCommand : ParticleCommand { - /** 环流中心。做蘑菇云时一般填帽子中心。 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** 主轴,默认 Y 轴。 */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** 主半径,也就是翻卷带离中心的距离。 */ - var ringRadius: Double = 3.0 - - /** 径向厚度。 */ - var radialThickness: Double = 1.2 - - /** 轴向厚度。 */ - var axialThickness: Double = 0.8 - - /** 翻卷力度,负数表示反向。 */ - var circulationStrength: Double = 0.35 - - /** 向外撑开的附加力度。 */ - var outwardStrength: Double = 0.0 - - /** 向上抬的附加力度。 */ - var upwardStrength: Double = 0.0 - - /** 往翻卷带回带/导向的力度。主要用于带外把速度重新导回卷动区。 */ - var followStrength: Double = 0.12 - - /** 单 tick 最大修正量。<=0 不限制。 */ - var maxStep: Double = 0.6 - - /** 是否按生命周期减弱。 */ - var useLifeCurve: Boolean = false - - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - ringRadius: Double = 3.0, - radialThickness: Double = 1.2, - axialThickness: Double = 0.8, - circulationStrength: Double = 0.35, - outwardStrength: Double = 0.0, - upwardStrength: Double = 0.0, - followStrength: Double = 0.12, - maxStep: Double = 0.6, - useLifeCurve: Boolean = false, - ) { - this.center = center - this.axis = axis - this.ringRadius = ringRadius - this.radialThickness = radialThickness - this.axialThickness = axialThickness - this.circulationStrength = circulationStrength - this.outwardStrength = outwardStrength - this.upwardStrength = upwardStrength - this.followStrength = followStrength - this.maxStep = maxStep - this.useLifeCurve = useLifeCurve - } - - fun center(v: Supplier) = apply { center = v } - - fun center(v: Vec3) = apply { center = Supplier { v } } - - fun center(x: Double, y: Double, z: Double) = apply { center = Supplier { Vec3(x, y, z) } } - - fun axis(v: Vec3) = apply { axis = v } - - fun axis(x: Double, y: Double, z: Double) = apply { axis = Vec3(x, y, z) } - - fun ringRadius(v: Double) = apply { ringRadius = v } - - fun radialThickness(v: Double) = apply { radialThickness = v } - - fun axialThickness(v: Double) = apply { axialThickness = v } - - /** 同时设置两个厚度,方便网页只留一个输入框。 */ - fun thickness(v: Double) = apply { - radialThickness = v - axialThickness = v - } - - fun circulationStrength(v: Double) = apply { circulationStrength = v } - - - fun outwardStrength(v: Double) = apply { outwardStrength = v } - - fun upwardStrength(v: Double) = apply { upwardStrength = v } - - fun followStrength(v: Double) = apply { followStrength = v } - - fun maxStep(v: Double) = apply { maxStep = v } - - fun useLifeCurve(v: Boolean) = apply { useLifeCurve = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val centerPos = center.get() - val ax = safeNormalize(axis) - val pos = particle.loc - val rel = pos.subtract(centerPos) - - val axialDistance = rel.dot(ax) - val planar = rel.subtract(ax.scale(axialDistance)) - val planarLen = planar.length() - - val radialDir = if (planarLen < 1e-9) { - anyPerp(ax) - } else { - planar.scale(1.0 / planarLen) - } - - val qr = planarLen - ringRadius - - val radialSize = radialThickness.coerceAtLeast(1e-6) - val axialSize = axialThickness.coerceAtLeast(1e-6) - - val localRadial = qr / radialSize - val localAxial = axialDistance / axialSize - val normalizedDistance = sqrt(localRadial * localRadial + localAxial * localAxial) - - val insideBand = normalizedDistance < 1.0 - val bandWeight = if (insideBand) smooth01(1.0 - normalizedDistance) else 0.0 - val outerDistance = (normalizedDistance - 1.0).coerceAtLeast(0.0) - val captureWeight = if (outerDistance > 0.0) { - 1.0 / (1.0 + outerDistance * outerDistance * 4.0) - } else { - 0.0 - } - val lifeMul = if (useLifeCurve && particle.lifetime > 0) { - (1.0 - particle.currentAge.toDouble() / particle.lifetime.toDouble()).coerceIn(0.0, 1.0) - } else { - 1.0 - } - if (lifeMul <= 1e-9) { - return - } - - val currentVelocity = if (data.velocity.lengthSqr() > 1e-12) data.velocity else particle.velocity - val currentSpeed = currentVelocity.length() - if (currentSpeed <= 1e-9) { - return - } - val currentDir = currentVelocity.scale(1.0 / currentSpeed) - - val circulationVector = radialDir.scale(-localAxial) - .add(ax.scale(localRadial)) - val toBandCenter = radialDir.scale(-localRadial).add(ax.scale(-localAxial)) - - var desiredFlow = Vec3.ZERO - - val circulationWeight = bandWeight * lifeMul - if (circulationStrength != 0.0 && circulationWeight > 1e-9 && circulationVector.lengthSqr() > 1e-12) { - desiredFlow = desiredFlow.add(circulationVector.scale(circulationStrength * circulationWeight)) - } - if (insideBand && outwardStrength != 0.0 && circulationWeight > 1e-9) { - desiredFlow = desiredFlow.add(radialDir.scale(outwardStrength * circulationWeight)) - } - if (insideBand && upwardStrength != 0.0 && circulationWeight > 1e-9) { - desiredFlow = desiredFlow.add(ax.scale(upwardStrength * circulationWeight)) - } - - val captureMul = captureWeight * lifeMul - if (followStrength != 0.0 && captureMul > 1e-9 && toBandCenter.lengthSqr() > 1e-12) { - desiredFlow = desiredFlow.add(normalizeOrZero(toBandCenter).scale(followStrength * captureMul)) - } - - val desiredFlowLen = desiredFlow.length() - if (desiredFlowLen <= 1e-9) { - return - } - val desiredDir = desiredFlow.scale(1.0 / desiredFlowLen) - val turnedDir = normalizeOrZero(currentDir.add(desiredDir.scale(desiredFlowLen))) - if (turnedDir.lengthSqr() <= 1e-12) { - return - } - - var dv = turnedDir.scale(currentSpeed).subtract(currentVelocity) - - val dvLen = dv.length() - if (maxStep > 0.0 && dvLen > maxStep) { - dv = dv.scale(maxStep / dvLen) - } - - data.velocity += dv - } - - private fun safeNormalize(v: Vec3): Vec3 { - val len = v.length() - return if (len < 1e-9) Vec3(0.0, 1.0, 0.0) else v.scale(1.0 / len) - } - - private fun normalizeOrZero(v: Vec3): Vec3 { - val len = v.length() - return if (len < 1e-9) Vec3.ZERO else v.scale(1.0 / len) - } - - private fun anyPerp(axis: Vec3): Vec3 { - val ref = if (abs(axis.y) < 0.9) Vec3(0.0, 1.0, 0.0) else Vec3(1.0, 0.0, 0.0) - val perp = axis.cross(ref) - val len = perp.length() - return if (len < 1e-9) Vec3(0.0, 0.0, 1.0) else perp.scale(1.0 / len) - } - - private fun smooth01(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - return clamped * clamped * (3.0 - 2.0 * clamped) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt deleted file mode 100644 index 4c86af8d..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/ParticleVortexCommand.kt +++ /dev/null @@ -1,161 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.world.phys.Vec3 -import java.util.function.Supplier -import kotlin.math.max - -/** - * 漩涡:让粒子围绕轴旋转,并可向轴线“吸入”,以及沿轴“上升/下沉”。 - * - * 常用组合: - * - swirlStrength > 0 : 打旋 - * - radialPull > 0 : 往轴线收拢(像龙卷风) - * - axialLift > 0 : 沿轴向上(像火焰/风柱) - */ -class ParticleVortexCommand() : ParticleCommand { - - /** 漩涡中心点(轴线穿过此点)。 */ - var center: Supplier = Supplier { Vec3.ZERO } - - /** 轴方向(自动 normalize)。 */ - var axis: Vec3 = Vec3(0.0, 1.0, 0.0) - - /** - * 切向旋转强度(每 tick 切向速度增量) - * - * - 0.1 ~ 0.8:柔和漩涡 - * - 0.8 ~ 3.0:明显龙卷感 - */ - var swirlStrength: Double = 0.8 - - /** - * 向轴线吸入的强度(每 tick 径向速度增量) - * - * - 0:不收拢(只打旋) - * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” - */ - var radialPull: Double = 0.35 - - /** - * 沿轴方向的升力(每 tick 轴向速度增量) - * - * - >0:沿轴正方向上升 - * - <0:沿轴反方向下沉 - */ - var axialLift: Double = 0.0 - - /** 衰减范围尺度(>0)。 */ - var range: Double = 10.0 - - /** 衰减幂次(>=1)。 */ - var falloffPower: Double = 2.0 - - /** 避免 r=0 的最小距离钳制 */ - var minDistance: Double = 0.2 - - /** - * @param center 漩涡中心(轴线穿过点) - * @param axis 轴方向(自动 normalize) - * @param swirlStrength 旋转强度(切向) - * @param radialPull 径向吸入强度 - * @param axialLift 轴向升力 - * @param range 衰减范围尺度 - * @param falloffPower 衰减幂次 - * @param minDistance 最小距离钳制,避免抖动/爆炸 - */ - constructor( - center: Supplier = Supplier { Vec3.ZERO }, - axis: Vec3 = Vec3(0.0, 1.0, 0.0), - swirlStrength: Double = 0.8, - radialPull: Double = 0.35, - axialLift: Double = 0.0, - range: Double = 10.0, - falloffPower: Double = 2.0, - minDistance: Double = 0.2, - ) : this() { - this.center = center - this.axis = axis - this.swirlStrength = swirlStrength - this.radialPull = radialPull - this.axialLift = axialLift - this.range = range - this.falloffPower = falloffPower - this.minDistance = minDistance - } - - /** 漩涡中心点(轴线穿过此点)。 */ - fun center(v: Supplier) = apply { center = v } - - /** 轴方向(自动 normalize)。 */ - fun axis(v: Vec3) = apply { axis = v } - - /** - * 切向旋转强度(每 tick 切向速度增量) - * - * - 0.1 ~ 0.8:柔和漩涡 - * - 0.8 ~ 3.0:明显龙卷感 - */ - fun swirlStrength(v: Double) = apply { swirlStrength = v } - - /** - * 向轴线吸入的强度(每 tick 径向速度增量) - * - * - 0:不收拢(只打旋) - * - 0.1 ~ 1.5:逐渐收拢形成“漏斗” - */ - fun radialPull(v: Double) = apply { radialPull = v } - - /** - * 沿轴方向的升力(每 tick 轴向速度增量) - * - * - >0:沿轴正方向上升 - * - <0:沿轴反方向下沉 - */ - fun axialLift(v: Double) = apply { axialLift = v } - - /** 衰减范围尺度(>0)。 */ - fun range(v: Double) = apply { range = v } - - /** 衰减幂次(>=1)。 */ - fun falloffPower(v: Double) = apply { falloffPower = v } - - /** 避免 r=0 的最小距离钳制 */ - fun minDistance(v: Double) = apply { minDistance = v } - - override fun execute(data: ControlableParticleData, particle: ControlableParticle) { - val pos = particle.loc - val ax = axis.normalize() - - // vector from center - val r = pos.subtract(center.get()) - - // remove axial component -> radial vector to axis line - val axialComp = ax.scale(r.dot(ax)) - val radial = r.subtract(axialComp) - - var dist = radial.length() - if (dist < 1e-9) dist = 0.0 - - val d = max(dist, minDistance) - val falloff = GraphMathHelper.inversePowerFalloff(d, range, falloffPower) - - // tangential direction - var tangential = ax.cross(radial) - val tLen = tangential.length() - if (tLen > 1e-9) tangential = tangential.scale(1.0 / tLen) - - // radial inward direction - var inward = Vec3.ZERO - val rLen = radial.length() - if (rLen > 1e-9) inward = radial.scale(-1.0 / rLen) - - val dv = tangential.scale(swirlStrength * falloff) - .add(inward.scale(radialPull * falloff)) - .add(ax.scale(axialLift * falloff)) - - data.velocity = data.velocity.add(dv) - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt deleted file mode 100644 index 618855d8..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierFloatKeyframe.kt +++ /dev/null @@ -1,10 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -data class BezierFloatKeyframe( - val time: Double, - val value: Double, - val outX: Double = 0.0, - val outY: Double = 0.0, - val inX: Double = 0.0, - val inY: Double = 0.0 -) diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt deleted file mode 100644 index b8838ff9..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/BezierKeyframeFloatCurve.kt +++ /dev/null @@ -1,250 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -import kotlin.math.abs -import kotlin.math.sqrt - -class BezierKeyframeFloatCurve( - frames: List = listOf(BezierFloatKeyframe(0.0, 0.0)) -) : FloatCurve { - private val keyframes: MutableList = - frames.map { it.withClampedTime() } - .sortedBy { it.time } - .toMutableList() - - init { - if (keyframes.isEmpty()) { - keyframes.add(BezierFloatKeyframe(0.0, 0.0)) - } - } - - fun setFrames(frames: List) = apply { - keyframes.clear() - keyframes.addAll( - frames.map { it.withClampedTime() } - .sortedBy { it.time } - ) - if (keyframes.isEmpty()) { - keyframes.add(BezierFloatKeyframe(0.0, 0.0)) - } - } - - fun addFrame( - time: Double, - value: Double, - outX: Double = 0.0, - outY: Double = 0.0, - inX: Double = 0.0, - inY: Double = 0.0 - ) = apply { - keyframes.add( - BezierFloatKeyframe( - time = time.coerceIn(0.0, 1.0), - value = value, - outX = outX, - outY = outY, - inX = inX, - inY = inY - ) - ) - keyframes.sortBy { it.time } - } - - /** - * 返回当前锚点和控制柄的不可变快照。 - * - * 示例:`CParticleCurve.fromFloatCurve(curve)` 使用此快照保留 GPU 控制柄。 - * 禁止:修改返回列表不会改变此曲线;需要修改时应调用 [setFrames]。 - * - * @return 按时间升序排列的当前关键帧 - */ - fun frames(): List = keyframes.toList() - - override fun sample(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - if (keyframes.size == 1) { - return keyframes[0].value - } - if (clamped <= keyframes.first().time) { - return keyframes.first().value - } - if (clamped >= keyframes.last().time) { - return keyframes.last().value - } - - for (i in 1 until keyframes.size) { - val prev = keyframes[i - 1] - val next = keyframes[i] - if (clamped <= next.time) { - return sampleSegment(prev, next, clamped) - } - } - return keyframes.last().value - } - - private fun sampleSegment(prev: BezierFloatKeyframe, next: BezierFloatKeyframe, time: Double): Double { - val span = next.time - prev.time - if (span <= EPSILON) { - return next.value - } - - val p0x = prev.time - val p1x = prev.time + prev.outX / 100.0 - val p2x = next.time + next.inX / 100.0 - val p3x = next.time - - val p0y = prev.value - val p1y = prev.value + prev.outY - val p2y = next.value + next.inY - val p3y = next.value - - val linearGuess = ((time - prev.time) / span).coerceIn(0.0, 1.0) - val u = solveParameterForX(time, p0x, p1x, p2x, p3x, linearGuess) - return cubicBezier(u, p0y, p1y, p2y, p3y) - } - - private fun solveParameterForX( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double, - guess: Double - ): Double { - if (p0 <= p1 && p1 <= p2 && p2 <= p3) { - return solveMonotonicParameter(targetX, p0, p1, p2, p3) - } - - val roots = mutableListOf() - val splitPoints = derivativeRoots(p0, p1, p2, p3) - .filter { it > EPSILON && it < 1.0 - EPSILON } - .plus(0.0) - .plus(1.0) - .sorted() - - for (i in 1 until splitPoints.size) { - val lo = splitPoints[i - 1] - val hi = splitPoints[i] - val flo = cubicBezier(lo, p0, p1, p2, p3) - targetX - val fhi = cubicBezier(hi, p0, p1, p2, p3) - targetX - when { - abs(flo) <= EPSILON -> roots.add(lo) - abs(fhi) <= EPSILON -> roots.add(hi) - flo < 0.0 && fhi > 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) - flo > 0.0 && fhi < 0.0 -> roots.add(solveBracketedParameter(targetX, p0, p1, p2, p3, lo, hi)) - } - } - - return roots.distinctBy { (it / EPSILON).toLong() } - .minByOrNull { abs(it - guess) } - ?: closestSampledParameter(targetX, p0, p1, p2, p3, guess) - } - - private fun solveMonotonicParameter( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double - ): Double { - var lo = 0.0 - var hi = 1.0 - var mid = 0.5 - repeat(32) { - mid = (lo + hi) * 0.5 - if (cubicBezier(mid, p0, p1, p2, p3) < targetX) { - lo = mid - } else { - hi = mid - } - } - return mid - } - - private fun solveBracketedParameter( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double, - startLo: Double, - startHi: Double - ): Double { - var lo = startLo - var hi = startHi - var mid = (lo + hi) * 0.5 - val increasing = cubicBezier(hi, p0, p1, p2, p3) >= cubicBezier(lo, p0, p1, p2, p3) - repeat(32) { - mid = (lo + hi) * 0.5 - val x = cubicBezier(mid, p0, p1, p2, p3) - if ((x < targetX) == increasing) { - lo = mid - } else { - hi = mid - } - } - return mid - } - - private fun derivativeRoots(p0: Double, p1: Double, p2: Double, p3: Double): List { - val a = -p0 + 3.0 * p1 - 3.0 * p2 + p3 - val b = 3.0 * p0 - 6.0 * p1 + 3.0 * p2 - val c = -3.0 * p0 + 3.0 * p1 - - val qa = 3.0 * a - val qb = 2.0 * b - val qc = c - if (abs(qa) <= EPSILON) { - return if (abs(qb) <= EPSILON) emptyList() else listOf(-qc / qb) - } - - val discriminant = qb * qb - 4.0 * qa * qc - if (discriminant < 0.0) { - return emptyList() - } - - val root = sqrt(discriminant) - return listOf( - (-qb - root) / (2.0 * qa), - (-qb + root) / (2.0 * qa) - ) - } - - private fun closestSampledParameter( - targetX: Double, - p0: Double, - p1: Double, - p2: Double, - p3: Double, - guess: Double - ): Double { - var best = guess - var bestError = abs(cubicBezier(guess, p0, p1, p2, p3) - targetX) - for (i in 0..64) { - val u = i / 64.0 - val error = abs(cubicBezier(u, p0, p1, p2, p3) - targetX) - if (error < bestError) { - best = u - bestError = error - } - } - return best - } - - private fun cubicBezier(t: Double, p0: Double, p1: Double, p2: Double, p3: Double): Double { - val u = 1.0 - t - val u2 = u * u - val t2 = t * t - return u2 * u * p0 + - 3.0 * u2 * t * p1 + - 3.0 * u * t2 * p2 + - t2 * t * p3 - } - - private fun BezierFloatKeyframe.withClampedTime(): BezierFloatKeyframe { - return copy(time = time.coerceIn(0.0, 1.0)) - } - - private companion object { - const val EPSILON = 1.0E-9 - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt deleted file mode 100644 index 2717be1f..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/ConstantFloatCurve.kt +++ /dev/null @@ -1,18 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -/** - * 常量曲线:全生命周期输出同一个值。 - * - * 典型用途: - * - 固定重力倍率 - * - 固定速度修正 - * - 作为其它更复杂曲线的默认占位 - */ -class ConstantFloatCurve( - /** - * 常量值。 - */ - var value: Double = 0.0 -) : FloatCurve { - override fun sample(t: Double): Double = value -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt deleted file mode 100644 index 2998ffa8..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatCurve.kt +++ /dev/null @@ -1,22 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -/** - * 生命周期曲线接口(标量)。 - * - * 约定: - * - 输入 `t` 是生命周期进度,通常期望在 `[0,1]` 范围内 - * - 实现类应尽量在内部做 `t` 的钳制,避免调用方传入异常值时不稳定 - * - * 用途: - * - 给粒子命令(如 Force/Velocity/Inherit)提供“随时间变化”的系数 - * - 统一不同曲线实现(常量、线性、关键帧)的采样入口 - */ -fun interface FloatCurve { - /** - * 采样曲线值。 - * - * @param t 生命周期进度(通常是 `currentAge / lifetime`) - * @return 曲线在该进度下的值 - */ - fun sample(t: Double): Double -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt deleted file mode 100644 index d8953cf5..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/FloatKeyframe.kt +++ /dev/null @@ -1,12 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -/** - * 标量关键帧。 - * - * @property time 关键帧时间点,建议使用 `[0,1]` 的生命周期归一化时间 - * @property value 关键帧数值 - */ -data class FloatKeyframe( - val time: Double, - val value: Double -) diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt deleted file mode 100644 index 56253661..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/KeyframeFloatCurve.kt +++ /dev/null @@ -1,78 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -import cn.coostack.cooparticlesapi.utils.GraphMathHelper - -/** - * 关键帧曲线(线性插值)。 - * - * 规则: - * - 帧会按 `time` 升序保存 - * - `t` 落在相邻关键帧之间时,使用线性插值 - * - `t` 小于首帧或大于尾帧时,返回边界帧值 - */ -class KeyframeFloatCurve( - frames: List = listOf(FloatKeyframe(0.0, 0.0)) -) : FloatCurve { - private val keyframes: MutableList = - frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } - .sortedBy { it.time } - .toMutableList() - - init { - if (keyframes.isEmpty()) { - keyframes.add(FloatKeyframe(0.0, 0.0)) - } - } - - /** - * 批量替换关键帧列表。 - * - * - 时间会被钳制到 `[0,1]` - * - 内部会自动按时间升序排序 - */ - fun setFrames(frames: List) = apply { - keyframes.clear() - keyframes.addAll( - frames.map { FloatKeyframe(it.time.coerceIn(0.0, 1.0), it.value) } - .sortedBy { it.time } - ) - if (keyframes.isEmpty()) { - keyframes.add(FloatKeyframe(0.0, 0.0)) - } - } - - /** - * 追加一个关键帧。 - * - * @param time 帧时间(会钳制到 `[0,1]`) - * @param value 帧值 - */ - fun addFrame(time: Double, value: Double) = apply { - keyframes.add(FloatKeyframe(time.coerceIn(0.0, 1.0), value)) - keyframes.sortBy { it.time } - } - - override fun sample(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - if (keyframes.size == 1) { - return keyframes[0].value - } - if (clamped <= keyframes.first().time) { - return keyframes.first().value - } - if (clamped >= keyframes.last().time) { - return keyframes.last().value - } - - for (i in 1 until keyframes.size) { - val prev = keyframes[i - 1] - val next = keyframes[i] - if (clamped <= next.time) { - val seg = (next.time - prev.time).coerceAtLeast(1e-9) - val alpha = (clamped - prev.time) / seg - return GraphMathHelper.lerp(alpha, prev.value, next.value) - } - } - return keyframes.last().value - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt deleted file mode 100644 index e11c75e5..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/command/curve/LinearFloatCurve.kt +++ /dev/null @@ -1,27 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.command.curve - -import cn.coostack.cooparticlesapi.utils.GraphMathHelper - -/** - * 线性曲线:在 `[0,1]` 区间内从 `from` 匀速过渡到 `to`。 - * - * 典型用途: - * - 生命周期内“线性增强/衰减” - * - 先快速验证一个效果趋势,再换成关键帧曲线精调 - */ -class LinearFloatCurve( - /** - * `t=0` 时的值。 - */ - var from: Double = 0.0, - - /** - * `t=1` 时的值。 - */ - var to: Double = 1.0 -) : FloatCurve { - override fun sample(t: Double): Double { - val clamped = t.coerceIn(0.0, 1.0) - return GraphMathHelper.lerp(clamped, from, to) - } -} 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 index d9319bd3..d93dff22 100644 --- 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 @@ -1,77 +1,9 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import com.ezylang.evalex.Expression import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 - -class BallWindDirection( - override var direction: Vec3, - var radius: Double, - var offset: RelativeLocation, -) : WindDirection { - override var relative: Boolean = false - override var windSpeedExpress: String = "1" - override fun loadEmitters(emitters: ParticleEmitters): WindDirection { - this.emitters = emitters - return this - } - - override fun hasLoadedEmitters(): Boolean { - return emitters != null - } - - private var emitters: ParticleEmitters? = null - - companion object { - @JvmStatic - val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, data -> - data as BallWindDirection - buf.writeVec3(data.direction) - buf.writeBoolean(data.relative) - buf.writeUtf(data.windSpeedExpress) - buf.writeVec3(data.offset.toVector()) - buf.writeDouble(data.radius) - }, { - val direction = it.readVec3() - val relative = it.readBoolean() - val express = it.readUtf() - val offset = RelativeLocation.of(it.readVec3()) - val radius = it.readDouble() - BallWindDirection(direction, radius, offset).apply { - this.relative = relative - this.windSpeedExpress = express - } - } - ) - const val ID = "ball" - } - - override fun getID(): String { - return ID - } - - override fun getWind(particlePos: Vec3): Vec3 { - if (relative) { - val pos = emitters?.pos ?: return direction - val dir = particlePos.subtract(pos) - val express = Expression(windSpeedExpress) - .with("l", dir.length()) - .evaluate().numberValue.toDouble() - dir.normalize().scale(express) - return dir - } - return direction - } - - override fun inRange(pos: Vec3): Boolean { - return emitters!!.pos.add(offset.toVector()).subtract(pos).length() <= radius - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC +object BallWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") } -} \ No newline at end of file +} 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 index cb6f21dc..07cd620c 100644 --- 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 @@ -1,92 +1,9 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind -import cn.coostack.cooparticlesapi.barrages.HitBox -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import com.ezylang.evalex.Expression import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 -import kotlin.math.exp - -class BoxWindDirection( - override var direction: Vec3, - var box: HitBox, - var offset: RelativeLocation, -) : WindDirection { - override var relative: Boolean = false - override var windSpeedExpress: String = "1" - override fun loadEmitters(emitters: ParticleEmitters): WindDirection { - this.emitters = emitters - return this - } - - private var emitters: ParticleEmitters? = null - - companion object { - @JvmStatic - val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, data -> - data as BoxWindDirection - buf.writeVec3(data.direction) - buf.writeBoolean(data.relative) - buf.writeUtf(data.windSpeedExpress) - buf.writeVec3(data.offset.toVector()) - buf.writeDouble(data.box.x1) - buf.writeDouble(data.box.x2) - buf.writeDouble(data.box.y1) - buf.writeDouble(data.box.y2) - buf.writeDouble(data.box.z1) - buf.writeDouble(data.box.z2) - }, { - val direction = it.readVec3() - val relative = it.readBoolean() - val express = it.readUtf() - val offset = RelativeLocation.of(it.readVec3()) - val box = HitBox( - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - ) - BoxWindDirection(direction, box, offset).apply { - this.relative = relative - this.windSpeedExpress = express - } - } - ) - const val ID = "box" - } - - override fun getID(): String { - return ID - } - - override fun hasLoadedEmitters(): Boolean { - return emitters != null - } - - override fun getWind(particlePos: Vec3): Vec3 { - if (relative) { - val pos = emitters?.pos ?: return direction - val dir = particlePos.subtract(pos) - val express = Expression(windSpeedExpress) - .with("l", dir.length()) - .evaluate().numberValue.toDouble() - dir.normalize().scale(express) - return dir - } - return direction - } - - override fun inRange(pos: Vec3): Boolean { - val ofBox = box.ofBox(emitters!!.pos.add(offset.toVector())) - return ofBox.contains(pos) - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC +object BoxWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") } -} \ No newline at end of file +} 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 index 13b2f8b6..77d7ce63 100644 --- 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 @@ -1,68 +1,9 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import com.ezylang.evalex.Expression import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 -import kotlin.math.exp - -class GlobalWindDirection( - override var direction: Vec3 -) : WindDirection { - override var relative: Boolean = false - override var windSpeedExpress: String = "1" - override fun loadEmitters(emitters: ParticleEmitters): WindDirection { - this.emitters = emitters - return this - } - - private var emitters: ParticleEmitters? = null - - companion object { - @JvmStatic - val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, data -> - buf.writeVec3(data.direction) - buf.writeBoolean(data.relative) - buf.writeUtf(data.windSpeedExpress) - }, { - val direction = it.readVec3() - val relative = it.readBoolean() - val express = it.readUtf() - GlobalWindDirection(direction).apply { - this.relative = relative - this.windSpeedExpress = express - } - } - ) - const val ID = "global" - } - - override fun getID(): String { - return ID - } - override fun hasLoadedEmitters(): Boolean { - return emitters != null - } - override fun getWind(particlePos: Vec3): Vec3 { - if (relative) { - val pos = emitters?.pos ?: return direction - val dir = particlePos.subtract(pos) - val express = Expression(windSpeedExpress) - .with("l", dir.length()) - .evaluate().numberValue.toDouble() - dir.normalize().scale(express) - return dir - } - return direction - } - - override fun inRange(pos: Vec3): Boolean { - return true - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC +object GlobalWindDirection : WindDirection { + override fun nextTick(): Double { + TODO("Not yet implemented") } -} \ No newline at end of file +} 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 index f662470b..e067abac 100644 --- 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 @@ -1,46 +1,46 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 - interface WindDirection { - /** - * 风向 - * 如果relative 为true 则此选项作废 - * direction.length() 代表风速大小 - * 如果设置为0向量 则代表没有风 - * relative为true 时代表粒子相对发射器位置为风向 - */ - var direction: Vec3 - var relative: Boolean - - /** - * relative 为false时此功能不启用 - * 若relative为true - * 则此参数代表风力大小 - * 表达式采用EvalEx3.5.0 - * 提供的变量 l (代表粒子与发射器的实际距离) - * 返回值是风速大小(direction.length()) - */ - var windSpeedExpress: String - - fun loadEmitters(emitters: ParticleEmitters): WindDirection - - fun hasLoadedEmitters(): Boolean - - fun getID(): String - - /** - * 获得当前位置的风速向量 - */ - fun getWind(particlePos: Vec3): Vec3 - - /** - * 粒子位置是否在可操作范围内 - */ - fun inRange(pos: Vec3): Boolean - - fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec -} \ No newline at end of file + 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 index ae33dc38..98e04479 100644 --- 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 @@ -1,57 +1,24 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.environment.wind -import cn.coostack.cooparticlesapi.utils.RelativeLocation import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 - object WindDirections { - private val packets = HashMap>() - - val GLOBAL = register("global", GlobalWindDirection.CODEC) - - val BOX = register("box", BoxWindDirection.CODEC) - - val BALL = register("ball", BallWindDirection.CODEC) - - /** - * 物理处理风力 - * @param v 先前移动方向 - * @param pos 在风场中的位置 (在粒子发射器中就是粒子位置) 如果超出范围则会返回0向量 - */ - fun handleWindForce( - wind: WindDirection, - pos: Vec3, - airDensity: Double, - dragCoefficient: Double, - crossSectionalArea: Double, - v: Vec3 = Vec3.ZERO, - ): Vec3 { - if (!wind.inRange(pos)) { - return Vec3.ZERO + 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") + } } - val windVec = wind.getWind(pos) - return if (windVec.lengthSqr() > 0) { - val relativeWind = windVec.subtract(v) - val windMagnitude = 0.5 * airDensity * dragCoefficient * - crossSectionalArea * relativeWind.lengthSqr() * 0.05 - relativeWind.normalize().scale(windMagnitude) - } else { - Vec3.ZERO - } - } - - fun getCodecFromID(id: String): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return packets[id]!! - } - - fun register( - id: String, - codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec - ): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - packets[id] = codec - return codec - } - - fun init() {} -} \ No newline at end of file + ) +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt deleted file mode 100644 index 0ed4fbff..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleCollideEvent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.phys.BlockHitResult - -/** - * 粒子碰撞方块事件 - * 用于复用物理粒子 - */ -class ParticleCollideEvent( - override var particle: ControlableParticle, - override var particleData: ControlableParticleData, - var res: BlockHitResult -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleColliderEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt deleted file mode 100644 index 9d7a3cad..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.network.particle.emitters.ParticleEmitters -import cn.coostack.cooparticlesapi.particles.ControlableParticle - -interface ParticleEvent { - /** - * 触发事件的粒子 - */ - var particle: ControlableParticle - var particleData: ControlableParticleData - - /** - * 取消接下来的粒子事件队列 - */ - var canceled: Boolean - - /** - * 用于方便存储 - * 获取事件ID - */ - fun getEventID(): String -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt deleted file mode 100644 index e6a1b67b..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandler.kt +++ /dev/null @@ -1,26 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -/** - * 自动注册 - * 事件处理器是单例模式 - */ -interface ParticleEventHandler : Comparable { - - fun handle(event: ParticleEvent) - - /** - * 详细见 - * 目标事件的EventID - * @see ParticleEvent.getEventID - */ - fun getTargetEventID(): String - - fun getHandlerID(): String - - fun getPriority(): Int - - override fun compareTo(other: ParticleEventHandler): Int { - return getPriority() - other.getPriority() - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt deleted file mode 100644 index 7a2a4446..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleEventHandlerManager.kt +++ /dev/null @@ -1,56 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.annotations.CooAutoRegister -import cn.coostack.cooparticlesapi.reflect.CooAPIScanner -import cn.coostack.cooparticlesapi.reflect.SimpleClassInfo -import cn.coostack.cooparticlesapi.utils.ReflectUtil -import java.lang.reflect.Modifier - -object ParticleEventHandlerManager { - private val registerHandlers = HashMap() - - fun getHandlerById(id: String): ParticleEventHandler? { - return registerHandlers[id] - } - - fun register(event: ParticleEventHandler) { - registerHandlers[event.getHandlerID()] = event - } - - fun hasRegister(id: String): Boolean = getHandlerById(id) != null - - private var handled = false - fun registerScanner() { - if (handled) { - return - } - val before = registerHandlers.size - CooParticlesConstants.logger.info("正在自动注册 ParticleEventHandler") - var count = 0 - val start = System.currentTimeMillis() - CooAPIScanner.getWithAnnotation( - CooAutoRegister::class.java - ).forEach { - count++ - findListenerHandlers(it) - } - val end = System.currentTimeMillis() - CooParticlesConstants.logger.info("EmittersEvents 注册完成 耗时 ${end - start} ms 扫描了 $count 个类 实际注册 :${registerHandlers.size - before}") - } - - private fun findListenerHandlers(target: SimpleClassInfo) { - val clazz = target.toClass() - if (!ParticleEvent::class.java.isAssignableFrom(clazz)) { - return - } - // 获取instance - val instance = - clazz.declaredFields.find { it.name == "INSTANCE" && Modifier.isStatic(it.modifiers) }?.get(null) - ?: clazz.getDeclaredConstructor() - .apply { isAccessible = true } - .newInstance() - register(instance as ParticleEventHandler) - } - -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt deleted file mode 100644 index a1a7f892..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleHitEntityEvent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.world.entity.Entity - -/** - * 粒子接触实体事件 - */ - -class ParticleHitEntityEvent( - override var particle: ControlableParticle, override var particleData: ControlableParticleData, var hit: Entity, -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleHitEntityEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt deleted file mode 100644 index 3175ae3c..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnGroundEvent.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.core.BlockPos -import net.minecraft.world.phys.BlockHitResult -import net.minecraft.world.phys.Vec3 - -/** - * 粒子接触地面事件 - */ -class ParticleOnGroundEvent( - override var particle: ControlableParticle, - override var particleData: ControlableParticleData, - /** - * 击中的方块 - */ - var hit: BlockPos, - /** - * 和方块的交点 - */ - var intersection: Vec3, - var res: BlockHitResult -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleOnGroundEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt deleted file mode 100644 index 991cc368..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/emitters/event/ParticleOnLiquidEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.emitters.event - -import cn.coostack.cooparticlesapi.network.particle.emitters.ControlableParticleData -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import net.minecraft.core.BlockPos - -/** - * 粒子接触实体事件 - */ -class ParticleOnLiquidEvent( - override var particle: ControlableParticle, - override var particleData: ControlableParticleData, - var hit: BlockPos -) : ParticleEvent { - override var canceled: Boolean = false - - companion object { - const val EVENT_ID = "ParticleOnLiquidEvent" - } - - override fun getEventID(): String { - return EVENT_ID - } -} \ No newline at end of file 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 index 023381d7..a59f8275 100644 --- 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 @@ -1,78 +1,9 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.type -import cn.coostack.cooparticlesapi.barrages.HitBox import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 -import kotlin.random.Random - -class BoxEmittersShootType(val box: HitBox) : EmittersShootType { - companion object { - @JvmStatic - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, type -> - type as BoxEmittersShootType - val box = type.box - buf.writeDouble(box.x1) - buf.writeDouble(box.y1) - buf.writeDouble(box.z1) - buf.writeDouble(box.x2) - buf.writeDouble(box.y2) - buf.writeDouble(box.z2) - - }, { - val box = HitBox( - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - it.readDouble(), - ) - BoxEmittersShootType(box) - } - ) - const val ID = "box" - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC - } - - val random = Random(System.currentTimeMillis()) - override fun getPositions(origin: Vec3, tick: Int, count: Int): List { - return List(count) { - val x = random.nextDouble( - box.x1, - box.x2 - ) - val y = random.nextDouble( - box.y1, - box.y2 - ) - val z = random.nextDouble( - box.z1, - box.z2 - ) - origin.add(Vec3(x, y, z)) - } - } - - override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { - if (enter.length() < 1e-7) { - // 随机速度 - val p = Vec3( - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0) - ) - return p - } - return enter +object BoxEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") } -} \ No newline at end of file +} 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 index 90a473ad..119e77bc 100644 --- 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 @@ -2,33 +2,53 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.type import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 - interface EmittersShootType { - /** - * 获取id 用于序列化 - */ - fun getID(): String + 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") + } +} - /** - * 用于序列化发射类型 - */ - fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec +object PointEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} - /** - * 获取生成粒子的位置 - * @param origin 添加offset后的位置 - * @param tick 发射器当前tick - * @param count 粒子数量 - * @return 可能返回多个粒子 - */ - fun getPositions(origin: Vec3, tick: Int, count: Int): List +object MathEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") + } +} - /** - * 获取粒子的初始方向 - * @param enter 输入的参数方向 - * @param pos 粒子生成位置 - * @param origin 粒子发射器的位置 - */ - fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 -} \ No newline at end of file +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 index dc25731f..b4ccc6a5 100644 --- 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 @@ -1,74 +1,27 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.type -import cn.coostack.cooparticlesapi.barrages.HitBox import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 - object EmittersShootTypes { - - private val types = HashMap>() - - fun register(id: String, codec: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec) { - types[id] = codec - } - - fun fromID(id: String) = types[id] - - @JvmStatic - fun point(): EmittersShootType = PointEmittersShootType() - - @JvmStatic - fun box(box: HitBox): EmittersShootType = BoxEmittersShootType(box) - - @JvmStatic - fun line(dir: Vec3, step: Double): EmittersShootType = LineEmittersShootType(dir, step) - - /** - * @param xe 粒子生成 x 相对于发射器的位置偏移量表达式 - * @param ye 粒子生成 y 相对于发射器的位置偏移量表达式 - * @param ze 粒子生成 z 相对于发射器的位置偏移量表达式 - * - * @param dxe 粒子生成时 初始速度向量x表达式 - * @param dye 粒子生成时 初始速度向量y表达式 - * @param dze 粒子生成时 初始速度向量z表达式 - */ - @JvmStatic - fun math( - xe: String, - ye: String, - ze: String, - dxe: String, - dye: String, - dze: String, - ): EmittersShootType = MathEmittersShootType() - .apply { - x = xe - y = ye - z = ze - dx = dxe - dy = dye - dz = dze - setup() - } - - init { - register( - PointEmittersShootType.ID, PointEmittersShootType.CODEC - ) - register( - BoxEmittersShootType.ID, - BoxEmittersShootType.CODEC + 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") + } + } ) - register( - MathEmittersShootType.ID, - MathEmittersShootType.CODEC - ) - register( - LineEmittersShootType.ID, - LineEmittersShootType.CODEC - ) - } - - fun init() {} -} \ No newline at end of file +} 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 index 1f468c38..0135d085 100644 --- 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 @@ -2,52 +2,8 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.type import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 - - -class LineEmittersShootType(val dir: Vec3, val step: Double) : EmittersShootType { - companion object { - @JvmStatic - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, type -> - type as LineEmittersShootType - buf.writeVec3(type.dir) - buf.writeDouble(type.step) - - }, { - val dir = it.readVec3() - val step = it.readDouble() - LineEmittersShootType(dir, step) - } - ) - const val ID = "line" - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC - } - - override fun getPositions( - origin: Vec3, - tick: Int, - count: Int - ): List { - return List(count) { - origin.add(dir.normalize().scale(it * step)) - } - } - - override fun getDefaultDirection( - enter: Vec3, - tick: Int, - pos: Vec3, - origin: Vec3 - ): Vec3 { - return enter +object LineEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") } -} \ No newline at end of file +} 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 index a13aaec6..fa01dc5f 100644 --- 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 @@ -1,196 +1,9 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.type -import com.ezylang.evalex.Expression import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 -import java.math.BigDecimal -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.FutureTask - -/** - * 基于EvalEx 表达式的运算 - * 支持的变量 - * 对于粒子生成位置 - * t -> tick - * c -> count - * i -> 当前索引值 - * 对于粒子初始速度 - * t -> tick - * x -> posX - * y -> posY - * z -> posZ - * ox -> origin.x - * oy -> origin.y - * oz -> origin.z - */ -class MathEmittersShootType : EmittersShootType { - companion object { - @JvmStatic - val CODEC = cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { buf, type -> - type as MathEmittersShootType - buf.writeUtf(type.x) - buf.writeUtf(type.y) - buf.writeUtf(type.z) - buf.writeUtf(type.dx) - buf.writeUtf(type.dy) - buf.writeUtf(type.dz) - }, { - MathEmittersShootType().apply { - x = it.readUtf() - y = it.readUtf() - z = it.readUtf() - dx = it.readUtf() - dy = it.readUtf() - dz = it.readUtf() - setup() - } - } - ) - const val ID = "math" - } - - /** - * 位置运算表达式 - */ - var x = "0" - var y = "0" - var z = "0" - - private var xe = Expression(x) - .with("i", 0) - .and("c", 0) - .and("t", 0) - private var ye = Expression(y) - .with("i", 0) - .and("c", 0) - .and("t", 0) - private var ze = Expression(z) - .with("i", 0) - .and("c", 0) - .and("t", 0) - - /** - * 初始方向表达式 - */ - var dx = "0" - var dy = "0" - var dz = "0" - - private var dxe = Expression(dx) - .with("t", 0) - - private var dye = Expression(dy) - .with("t", 0) - - private var dze = Expression(dz) - .with("t", 0) - - fun setup() { - xe = Expression(x) - .with("i", 0) - .and("c", 0) - .and("t", 0) - ye = Expression(y) - .with("i", 0) - .and("c", 0) - .and("t", 0) - ze = Expression(z) - .with("i", 0) - .and("c", 0) - .and("t", 0) - dxe = Expression(dx) - .with("t", 0) - - dye = Expression(dy) - .with("t", 0) - - dze = Expression(dz) - .with("t", 0) - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC - } - - override fun getPositions( - origin: Vec3, - tick: Int, - count: Int - ): List { - - return List(count) { - val taskX = CompletableFuture.supplyAsync { - xe.with("t", tick) - .with("c", count) - .with("i", it) - .evaluate().numberValue.toDouble() - } - - val taskY = CompletableFuture.supplyAsync { - ye.with("t", tick) - .with("c", count) - .with("i", it) - .evaluate().numberValue.toDouble() - } - - val taskZ = CompletableFuture.supplyAsync { - ze.with("t", tick) - .and("c", count) - .and("i", it) - .evaluate().numberValue.toDouble() - } - CompletableFuture.allOf(taskX, taskY, taskZ).join() - origin.add(Vec3(taskX.get(), taskY.get(), taskZ.get())) - } - } - - override fun getDefaultDirection( - enter: Vec3, - tick: Int, - pos: Vec3, - origin: Vec3 - ): Vec3 { - val x = CompletableFuture.supplyAsync { - dxe.with("t", tick) - .and("x", pos.x) - .and("y", pos.y) - .and("z", pos.z) - .and("ox", origin.x) - .and("oy", origin.y) - .and("oz", origin.z) - .evaluate().numberValue.toDouble() - } - val y = CompletableFuture.supplyAsync { - dye.with("t", tick) - .and("x", pos.x) - .and("y", pos.y) - .and("z", pos.z) - .and("ox", origin.x) - .and("oy", origin.y) - .and("oz", origin.z) - .evaluate().numberValue.toDouble() - } - val z = CompletableFuture.supplyAsync { - dze.with("t", tick) - .and("x", pos.x) - .and("y", pos.y) - .and("z", pos.z) - .and("ox", origin.x) - .and("oy", origin.y) - .and("oz", origin.z) - .evaluate().numberValue.toDouble() - } - CompletableFuture.allOf(x, y, z).join() - return enter.add( - x.get(), y.get(), z.get() - ) +object MathEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") } -} \ No newline at end of file +} 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 index 040a9a18..a2da4dd7 100644 --- 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 @@ -2,47 +2,8 @@ package cn.coostack.cooparticlesapi.network.particle.emitters.type import net.minecraft.network.PacketByteBuf -import net.minecraft.world.phys.Vec3 -import kotlin.random.Random - -class PointEmittersShootType : EmittersShootType { - val random = Random(System.currentTimeMillis()) - - companion object { - @JvmStatic - val CODEC: cn.coostack.cooparticlesapi.network.packet.api.CommonCodec = - cn.coostack.cooparticlesapi.network.packet.api.CommonCodec.of( - { b, p -> }, { - PointEmittersShootType() - } - ) - const val ID = "point" - } - - override fun getID(): String { - return ID - } - - override fun getCodec(): cn.coostack.cooparticlesapi.network.packet.api.CommonCodec { - return CODEC - } - - override fun getPositions(origin: Vec3, tick: Int, count: Int): List { - return List(count) { - origin - } - } - - override fun getDefaultDirection(enter: Vec3, tick: Int, pos: Vec3, origin: Vec3): Vec3 { - if (enter.length() < 1e-7) { - // 随机速度 - val p = Vec3( - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0), - random.nextDouble(-1.0, 1.0) - ) - return p - } - return enter +object PointEmittersShootType : EmittersShootType { + override fun nextShoot(): Pair { + TODO("Not yet implemented") } -} \ No newline at end of file +} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt deleted file mode 100644 index f98bbc30..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleGroupStyle.kt +++ /dev/null @@ -1,573 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C -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.cparticle.CParticleSystem -import cn.coostack.cooparticlesapi.cparticle.compat.CParticleControlable -import cn.coostack.cooparticlesapi.cparticle.compat.CParticleDisplayer -import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import cn.coostack.cooparticlesapi.particles.ControlableParticle -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.particles.control.ControlParticleManager -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.particles.control.ParticleControler -import cn.coostack.cooparticlesapi.particles.control.RemoveReason -import cn.coostack.cooparticlesapi.particles.control.group.ControlableParticleGroup -import cn.coostack.cooparticlesapi.particles.impl.ControlableEndRodEffect -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import cn.coostack.cooparticlesapi.utils.Math3DUtil -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap -import kotlin.collections.set -import kotlin.math.PI - -/** 客户端渲染和服务端处理都用这个类 */ -@Deprecated("使用ParticleComposition") -abstract class ParticleGroupStyle(var visibleRange: Double = 32.0, val uuid: UUID = UUID.randomUUID()) : - Controlable, ServerControler, Tickable { - var world: Level? = null - var pos: Vec3 = Vec3.ZERO - var client = false - var rotate = 0.0 - var axis = RelativeLocation.yAxis() - var scale = 1.0 - set(value) { - field = value.coerceAtLeast(0.001) - } - - var positionDirty = false - - - /** - * 上一次更新的游戏时间 - * > 用来解决回放的时候会产生额外的粒子 或者导致缺少粒子 (额外tick) - */ - - var lastUpdatedGameTime = 0L - - /** 生成粒子样式的时间 可能会直接跳转到其他时候 然后这个时候这个参数就需要进行同步 */ - var displayedTime = 0L - - /** 自动发包同步到客户端 可能会占据大量带宽 */ - var autoToggle = false - - var displayed = false - internal set - internal var valid = true - internal val invokeQueue = ArrayList Unit>() - internal val postInvokeQueue = ArrayList Unit>() - val particles = ConcurrentHashMap>() - val particleLocations = ConcurrentHashMap, RelativeLocation>() - private val cParticleSystems = LinkedHashMap() - private val cParticleContainers = LinkedHashSet>() - - internal fun getCParticleSystems(): List { - removeReleasedCParticleSystems() - return cParticleSystems.keys.toList() - } - - internal fun getCParticleContainers(): List> = - cParticleContainers.toList() - - protected fun prepareCParticleDisplayer(displayer: ParticleDisplayer, capacity: Int) { - if (displayer is CParticleDisplayer) { - displayer.bindDedicatedSystemIfAbsent("style/$uuid", capacity, pos) - } - } - - protected fun registerCParticleNode(controler: Controlable<*>) { - removeReleasedCParticleSystems() - when (controler) { - is CParticleControlable -> cParticleSystems.merge(controler.system, 1, Int::plus) - is ParticleComposition, - is ParticleGroupStyle, - is ControlableParticleGroup -> cParticleContainers.add(controler) - } - } - - protected fun unregisterCParticleNode(controler: Controlable<*>) { - when (controler) { - is CParticleControlable -> { - val count = cParticleSystems[controler.system] ?: return - if (count <= 1) cParticleSystems.remove(controler.system) - else cParticleSystems[controler.system] = count - 1 - } - - is ParticleComposition, - is ParticleGroupStyle, - is ControlableParticleGroup -> cParticleContainers.remove(controler) - } - } - - private fun removeReleasedCParticleSystems() { - cParticleSystems.keys.removeIf { it.released } - } - - override fun isValid(): Boolean { - return valid - } - - /** 当粒子组合初始化时, 存储1倍缩放粒子组与原点的距离 */ - val particleDefaultLength = ConcurrentHashMap() - - - abstract fun getCurrentFrames(): Map - - abstract fun onDisplay() - - /** - * 服务器同步到客户端时, 执行的代码 基类已经存储的参数 如 pos, world ,rotate, axis, scale, uuid 无需同步 - * 自定义的其他参数需要同步 - * - * 如果 autoToggle = true 则会在每个tick都会执行发包 反复调用此方法 - */ - abstract fun writePacketArgs(): Map> - - /** - * 客户端接受服务器的同步时, 使用的代码 - * - * 同步内容如下 -> 客户端创建此类时 输入的基本参数 包括了你在 writePacketArgs输入的参数 客户端在类内存在其余更改时 - * 服务器传入的参数 比如自己设定的一些其他参数值 - * - * 无需处理以下参数 pos world rotate axis scale uuid 其余参数自行处理 - */ - abstract fun readPacketArgs(args: Map>) - - open fun beforeDisplay(styles: Map) { - } - - override fun addPreTickAction(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { - invokeQueue.add(action) - return this - } - - override fun addPreTickActionPost(action: ParticleGroupStyle.() -> Unit): ParticleGroupStyle { - postInvokeQueue.add(action) - return this - } - - override fun controlUUID(): UUID { - return uuid - } - - override fun rotateToPoint(to: RelativeLocation) { - Math3DUtil.rotatePointsToPoint( - particleLocations.values.toList(), to, axis - ) - axis = to - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to) - ) - ) - } - } - - override fun rotateToWithAngle(to: RelativeLocation, radian: Double) { -// Math3DUtil.rotateAsAxis( -// particleLocations.values.toList(), axis, angle -// ) -// Math3DUtil.rotatePointsToPoint( -// particleLocations.values.toList(), to, axis -// ) - Math3DUtil.rotateToWithRoll( - particleLocations.values.toList(), axis, to, radian - ) - axis = to - this.rotate += radian - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to), - "rotate_angle" to ParticleControlerDataBuffers.double(radian) - ) - ) - } - } - - override fun rotateAsAxis(angle: Double) { - Math3DUtil.rotateAsAxis( - particleLocations.values.toList(), axis, angle - ) - this.rotate += angle - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "rotate_angle" to ParticleControlerDataBuffers.double(angle) - ) - ) - } - } - - override fun teleportTo(pos: Vec3) { - this.pos = pos - toggleRelative() - if (!client) { - // 同步到其他客户端 - change( - mapOf( - "teleport" to ParticleControlerDataBuffers.vec3d(pos) - ) - ) - } - } - - override fun teleportTo(x: Double, y: Double, z: Double) { - teleportTo(Vec3(x, y, z)) - } - - override fun remove() { - clear(false) - if (!client) { - // 同步到其他客户端 - ParticleStyleManager.filterVisiblePlayer(this).forEach { - val player = world!!.getPlayerByUUID(it) as? ServerPlayer - player ?: return@forEach - CooParticlesServices.SERVER_NETWORK.send( - PacketParticleStyleS2C( - uuid, ControlType.REMOVE, mapOf() - ), player - ) - } - } - } - - /** Controler需求 */ - override fun getControlObject(): ParticleGroupStyle { - return this - } - - /** ServerControler需求 */ - override fun getValue(): ParticleGroupStyle { - return this - } - - /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ - fun change(toggleMethod: ParticleGroupStyle.() -> Unit, args: Map>) { - if (client) { - return - } - toggleMethod(this) - // 发包 - ParticleStyleManager.filterVisiblePlayer(this).forEach { - val player = world!!.getPlayerByUUID(it) ?: return@forEach - CooParticlesServices.SERVER_NETWORK.send( - PacketParticleStyleS2C( - uuid, ControlType.CHANGE, args - ), player as ServerPlayer - ) - } - } - - /** 当服务器出现一些不是来源于类内自发的更改时 (外部类更改) 请执行此代码 除非你开启了 autoToggle */ - fun change(args: Map>) { - change({}, args) - } - - open fun display(pos: Vec3, world: Level) { - if (displayed) { - return - } - displayed = true - this.displayedTime = world.gameTime - this.lastUpdatedGameTime = world.gameTime - this.pos = pos - this.world = world - this.client = world.isClientSide - if (!client) { - // 服务器只负责数据同步 不负责粒子生成 - onDisplay() - return - } - flush() - onDisplay() - } - - open fun flush() { - if (particles.isNotEmpty()) { - clear(true) - } - displayParticles() - } - - open fun toggleRelative() { - if (!client) { - return - } - val iterator = particleLocations.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - val particle = entry.key - val rl = entry.value - particle.teleportTo( - pos.add( - rl.toVector() - ) - ) - } - positionDirty = false - } - - - private fun spawnSingleParticle(data: StyleData, rel: RelativeLocation) { - val displayer = data.displayerBuilder(data.uuid) - if (displayer !is ParticleDisplayer.SingleParticleDisplayer) return - val controler = ControlParticleManager.createControl(data.uuid) - controler.applyInitializedAction(data.particleHandler) - val toPos = Vec3(pos.x + rel.x, pos.y + rel.y, pos.z + rel.z) - displayer.display(toPos, world as ClientLevel) ?: return - data.particleControlerHandler(controler) - // 把粒子丢回生成列表 - particles[data.uuid] = controler - particleLocations[controler] = rel - } - - override fun tick() { - if (!displayed || !valid) { - clear(false) - return - } - val current = world!!.gameTime - if (!displayed || !valid) { - clear(false) - return - } - invokeQueue.forEach { - it(this) - } - - val iterator = particles.iterator() - while (iterator.hasNext()) { - val style = iterator.next() - when (val value = style.value) { - is Tickable<*> -> { - value.tick() - } - } - } - postInvokeQueue.forEach { - it(this) - } - this.lastUpdatedGameTime = current - } - - fun toggleScale(locations: Map) { - if (!valid) { - // remove过后 无法同步 - return - } - if (particleDefaultLength.isEmpty()) { - locations.forEach { - val uuid = it.key.uuid - particleDefaultLength[uuid] = it.value.length() - } - } - locations.forEach { - val uuid = it.key.uuid - val len = particleDefaultLength[uuid] ?: return@forEach - if (len <= 0.0) { - return@forEach - } - val value = it.value - value.multiply(len * scale / value.length()) - } - } - - open fun scale(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale can not be less than zero") - return - } - scale = new - // 如果没有创建, 那么此处的环境100%是创建此对象时使用的环境 - // 多为服务端(除非有人使在Client环境创建了这个类) - if (displayed) { - toggleScaleDisplayed() - } - // 发包有效 - if (!valid) { - // remove过后 无法同步 - return - } - if (!client) { - change(mapOf("scale" to ParticleControlerDataBuffers.double(new))) - } - } - - protected open fun toggleScaleDisplayed() { - particleLocations.forEach { - val uuid = it.key.controlUUID() - val len = particleDefaultLength[uuid]!! - val value = it.value - if (len in -1e-3..1e-3) return@forEach - value.multiply(len * scale / value.length()) - } - toggleRelative() - } - - override fun spawn(world: Level, pos: Vec3) { - if (world !is ServerLevel) return - ParticleStyleManager.spawnStyle(world, pos, this) - } - - open fun preRotateTo(map: Map, to: RelativeLocation) { - Math3DUtil.rotatePointsToPoint( - map.values.toList(), to, axis - ) - this.axis = to - } - - open fun preRotateAsAxis(map: Map, axis: RelativeLocation, angle: Double) { - Math3DUtil.rotateAsAxis( - map.values.toList(), axis, angle - ) - this.axis = axis - } - - open fun preRotateAsAxis(map: Map, angle: Double) { - Math3DUtil.rotateAsAxis( - map.values.toList(), axis, angle - ) - } - - private fun displayParticles() { - val locations = getCurrentFrames() - beforeDisplay(locations) - toggleScale(locations) - Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) - locations.forEach { - val data = it.key - val uuid = it.key.uuid - val rl = it.value - val displayer = it.key.displayerBuilder(uuid) - prepareCParticleDisplayer(displayer, locations.size) - if (displayer is ParticleDisplayer.SingleParticleDisplayer) { - val controler = ControlParticleManager.createControl(uuid) - controler.applyInitializedAction(data.particleHandler) - } - val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) - val controler = displayer.display(toPos, world as ClientLevel) ?: return@forEach - if (controler is ParticleControler) { - data.particleControlerHandler(controler) - } - registerCParticleNode(controler) - - particles[uuid] = controler - particleLocations[controler] = rl - } - } - - override fun remove(reason: RemoveReason) { - remove() - } - - open internal fun clear(valid: Boolean) { - particles.forEach { - it.value.remove() - } - particles.clear() - particleLocations.clear() - particleDefaultLength.clear() - cParticleSystems.clear() - cParticleContainers.clear() - this.valid = valid - } - - open class StyleData( - val displayerBuilder: (UUID) -> ParticleDisplayer, - ) { - val uuid: UUID = UUID.randomUUID() - var particleHandler: ControlableParticle.() -> Unit = {} - var particleControlerHandler: ParticleControler.() -> Unit = {} - fun withParticleHandler( - builder: ControlableParticle.() -> Unit - ): StyleData { - particleHandler = builder - return this - } - - fun withParticleControlerHandler( - builder: ParticleControler.() -> Unit - ): StyleData { - particleControlerHandler = builder - return this - } - } - - /** 为了提高傻逼StyleData的复用性 专门设置此类 */ - open class StyleDataBuilder() { - private var displayerBuilder: (UUID) -> ParticleDisplayer = - { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } - private val particleHandlers = mutableListOf Unit>() - private val particleControlerHandlers = mutableListOf Unit>() - fun addParticleHandler( - builder: ControlableParticle.() -> Unit - ): StyleDataBuilder { - particleHandlers.add(builder) - return this - } - - fun addParticleControlerHandler( - builder: ParticleControler.() -> Unit - ): StyleDataBuilder { - particleControlerHandlers.add(builder) - return this - } - - fun clearParticleHandlers(): StyleDataBuilder { - particleHandlers.clear() - return this - } - - fun clearParticleControlers(): StyleDataBuilder { - particleControlerHandlers.clear() - return this - } - - fun removeHandler(index: Int): StyleDataBuilder { - if (index in particleHandlers.indices) particleHandlers.removeAt(index) - return this - } - - fun removeParticleControler(index: Int): StyleDataBuilder { - if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) - return this - } - - fun displayer(builder: (UUID) -> ParticleDisplayer): StyleDataBuilder { - this.displayerBuilder = builder - return this - } - - fun build(): StyleData = StyleData(displayerBuilder) - .withParticleHandler { - particleHandlers.forEach { it() } - }.withParticleControlerHandler { - particleControlerHandlers.forEach { it() } - } - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt deleted file mode 100644 index 25e9c85d..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleShapeStyle.kt +++ /dev/null @@ -1,222 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper -import net.minecraft.client.particle.ParticleRenderType -import net.minecraft.world.entity.player.Player -import net.minecraft.world.phys.Vec3 -import java.util.UUID - -/** - * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) - * 专门创建此类 - * - * 此类没有Provider 因此无法单独生成 - * - * 继承此类的目的是为这个类创建一个专属形状的子类 - * 这种子类可以拥有Provider - */ -open class ParticleShapeStyle(uuid: UUID) : - ParticleGroupStyle(64.0, uuid) { - var scaleHelper: ScaleHelper? = null - - /** - * 增加可复用性 - */ - private val displayInvokes = mutableListOf Unit>() - private val beforeDisplayInvokes = mutableListOf) -> Unit>() - private val pointBuilders = LinkedHashMap StyleData>() - - /** - * 通过判断(外层) 提供的状态工具来决定是否反转缩放 - * 在toggleDisplay执行这个方法 - */ - val reverseFunctionFromStatus: (ParticleShapeStyle, StyleStatusHelper) -> Unit = function@{ it, displayStatus -> - // 必须设置缩放工具 - if (scaleHelper == null) return@function - it.addPreTickAction { - if (it.scaleReversed && it.scaleHelper!!.isZero()) { - it.remove() - return@addPreTickAction - } - if (displayStatus.displayStatus != 2) { - return@addPreTickAction - } - if (!it.scaleReversed) { - it.scaleReversed(false) - } - } - } - var spawnAge = 0 - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - private set - - fun appendBuilder(pointsBuilder: PointsBuilder, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { - pointBuilders[pointsBuilder] = dataBuilder - return this - } - - fun appendPoint(point: RelativeLocation, dataBuilder: (RelativeLocation) -> StyleData): ParticleShapeStyle { - pointBuilders[ - PointsBuilder().also { it.addPoint(point) } - ] = dataBuilder - return this - } - - /** - * @param max 直接让scale从最大开始 - */ - fun scaleReversed(max: Boolean): ParticleShapeStyle { - scaleHelper ?: return this - scaleReversed = true - if (max) { - scaleHelper!!.toggleScale(scaleHelper!!.maxScale) - } - return this - } - - fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): ParticleShapeStyle { - scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - fun loadScaleHelperBezierValue( - minScale: Double, - maxScale: Double, - scaleTick: Int, - c1: RelativeLocation, - c2: RelativeLocation - ): ParticleShapeStyle { - scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - /** - * 在display之前执行 - * 可以多次执行此方法, 不会发生方法覆盖 - */ - fun toggleOnDisplay(toggleMethod: ParticleShapeStyle.() -> Unit): ParticleShapeStyle { - displayInvokes.add(toggleMethod) - return this - } - - /** - * 在生成粒子之前执行 - */ - fun toggleBeforeDisplay(toggleMethod: ParticleShapeStyle.(Map) -> Unit): ParticleShapeStyle { - beforeDisplayInvokes.add(toggleMethod) - return this - } - - override fun beforeDisplay(styles: Map) { - beforeDisplayInvokes.forEach { it(styles) } - } - - override fun getCurrentFrames(): Map { - val res = HashMap() - pointBuilders.forEach { entry -> - res.putAll(entry.key.createWithStyleData { entry.value(it) }) - } - return res - } - - override fun isValid(): Boolean { - return valid - } - - override fun onDisplay() { - displayInvokes.forEach { - it() - } - addPreTickAction { - spawnAge++ - if (!scalePreTick || scaleHelper == null) { - return@addPreTickAction - } - - if (scaleReversed) { - scaleHelper!!.doScaleReversed() - } else { - scaleHelper!!.doScale() - } - } - } - - override fun writePacketArgs(): Map> { - return mapOf() - } - - override fun readPacketArgs(args: Map>) { - } - - fun fastRotateToPlayerView(player: Player) { - rotateToPoint(RelativeLocation.of(player.forward)) - } - - fun fastStyleData(color: Vec3, displayer: (UUID) -> ParticleDisplayer): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - } - } - - fun fastStyleData(color: Vec3, sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - } - } - - fun fastStyleData( - color: Vec3, - sheet: ParticleRenderType, - size: Float, - displayer: (UUID) -> ParticleDisplayer - ): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - } - } - - fun fastStyleData( - color: Vec3, - sheet: ParticleRenderType, - size: Float, - alpha: Float, - displayer: (UUID) -> ParticleDisplayer - ): StyleData { - return StyleData(displayer).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - this.particleAlpha = alpha - } - } - - fun fastStyleData(sheet: ParticleRenderType, displayer: (UUID) -> ParticleDisplayer): StyleData { - return StyleData(displayer).withParticleHandler { - this.textureSheet = sheet - } - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt deleted file mode 100644 index dd68ba0c..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleManager.kt +++ /dev/null @@ -1,259 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.CooParticlesAPI -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleStyleS2C -import cn.coostack.cooparticlesapi.particles.control.ControlType -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import net.minecraft.client.Minecraft -import net.minecraft.server.level.ServerPlayer -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 - -/** - * TODO 重构Style使得让自动更新默认打开(如果更新时发现不存在那就重新生成) - * - * @constructor Create empty Particle style manager - */ -@Deprecated("使用ParticleComposition") -object ParticleStyleManager { - - /** - * 服务端拥有的 -> (Server View)客户层是empty - */ - val serverViewStyles = ConcurrentHashMap() - - /** - * 玩家可以看见的 style - */ - internal val visible = ConcurrentHashMap>() - - /** - * 当前客户端可见的 (Client View)服务层是empty - */ - val clientViewStyles = ConcurrentHashMap() - - private val registerBuilders = - HashMap, ParticleStyleProvider>() - - /** - * 在ClientModInitializer 注册ParticleStyle 用于服务器同步 - */ - // Keep this as an instance method for binary compatibility with existing consumers. - fun register( - type: Class, - provider: ParticleStyleProvider - ) { - registerBuilders[type] = provider - } - - fun getBuilder(type: Class): ParticleStyleProvider? { - return registerBuilders[type] - } - - @JvmStatic - fun spawnStyle(world: Level, pos: Vec3, style: ParticleGroupStyle) { - if (world.isClientSide) { - // 生成粒子 - style.display(pos, world) - clientViewStyles[style.uuid] = style - return - } - style.display(pos, world) - serverViewStyles[style.uuid] = style - // 发送数据包 -> 包括type - world.players().filter { style.pos.distanceTo(it.position()) <= style.visibleRange }.forEach { - addStylePlayerView(it as ServerPlayer, style) - } - } - - - fun doTickClient() { - val player = Minecraft.getInstance().player ?: return - if (player.isDeadOrDying) { - clientViewStyles.clear() - return - } - val iterator = clientViewStyles.iterator() - while (iterator.hasNext()) { - val style = iterator.next().value - style.tick() - if (!style.valid) { - iterator.remove() - } - } - } - - fun doTickServer() { - val iterator = serverViewStyles.iterator() - while (iterator.hasNext()) { - val style = iterator.next().value - // 更新可见性 - upgradeVisible(style) - style.tick() - if (style.autoToggle) { - style.world!!.players().forEach { - val visibleSet = visible.getOrPut(it.uuid) { HashSet() } - // 不可见的粒子没有必要同步 - if (!visibleSet.contains(style)) { - return@forEach - } - CooParticlesServices.SERVER_NETWORK.send( - buildAutoTogglePacket(style), - it as ServerPlayer, - ) - } - } - if (!style.valid) { - filterVisiblePlayer(style).forEach { - val player = style.world!!.getPlayerByUUID(it) ?: return@forEach - removeGroupPlayerView(player as ServerPlayer, style) - visible[it]?.remove(style) - } - iterator.remove() - } - } - val playerVisibleIterator = visible.iterator() - while (playerVisibleIterator.hasNext()) { - val playerUUID = playerVisibleIterator.next().key - // 判断玩家是否在线 - val server = CooParticlesAPI.serverOrNull ?: return - val player = server.playerList.getPlayer(playerUUID) - if (player == null) { - playerVisibleIterator.remove() - } - } - } - - fun filterVisiblePlayer(group: ParticleGroupStyle): Set { - val set = HashSet() - visible.forEach { - if (group in it.value) { - set.add(it.key) - } - } - return set - } - - private fun upgradeVisible(style: ParticleGroupStyle) { - val server = CooParticlesAPI.serverOrNull ?: return - server.playerList.players.forEach { p -> - val visibleSet = visible.getOrPut(p.uuid) { HashSet() } - if (p.level().dimension() != style.world?.dimension()) { - // 世界转换 - if (style in visibleSet) { - removeGroupPlayerView(p, style) - visibleSet!!.remove(style) - } - return@forEach - } - if (p.isDeadOrDying) { - if (style in visibleSet) { - removeGroupPlayerView(p, style) - visibleSet!!.remove(style) - } - return@forEach - } - if (style.pos.distanceTo(p.position()) <= style.visibleRange) { - // 防止重复添加(发包) - if (style in visibleSet) { - return@forEach - } - addStylePlayerView(p, style) - } else { - if (style in visibleSet) { - removeGroupPlayerView(p, style) - } - // 超过范围 - visibleSet.remove(style) - } - } - } - - /** - * 在该玩家的视角中移除这些粒子 - * @param target - * @param targetGroup - */ - private fun removeGroupPlayerView(target: ServerPlayer, targetGroup: ParticleGroupStyle) { - // 发包给玩家 - val packet = PacketParticleStyleS2C( - targetGroup.uuid, - ControlType.REMOVE, mapOf() - ) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - /* - * @param target - * @param targetStyle - */ - private fun addStylePlayerView( - target: ServerPlayer, - targetStyle: ParticleGroupStyle, - ) { - // 修复一个包发送给一个玩家两次 - val visibleSet = visible.getOrPut(target.uuid) { HashSet() } - visibleSet.add(targetStyle) - // 发包给玩家 - val packet = buildCreatePacket(targetStyle, targetStyle.pos) - CooParticlesServices.SERVER_NETWORK.send(packet, target) - } - - private fun buildAutoTogglePacket( - style: ParticleGroupStyle, - ): PacketParticleStyleS2C { - - /** - * SequencedParticleStyle - * 你赢了 - * addMultiple的index 能从 7 自动同步成 0 - * - * 一看 server side 诶 index没有问题 - * 一看args 诶 index没有问题 - * 但是呢 - * 传入客户端之后啊 - * 7 变成 0 了捏 - * 不知道的以为是Packet的 CODEC被你妈吃数据包了 - */ - - return PacketParticleStyleS2C( - style.uuid, - ControlType.CHANGE, - mapOf( - *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() - ) - ) - } - - private fun buildCreatePacket(style: ParticleGroupStyle, pos: Vec3): PacketParticleStyleS2C = - PacketParticleStyleS2C( - style.uuid, - ControlType.CREATE, - mapOf( - "style_type" to ParticleControlerDataBuffers.string(style::class.java.name), - "pos" to ParticleControlerDataBuffers.vec3d(pos), - "rotate" to ParticleControlerDataBuffers.double(style.rotate), - "axis" to ParticleControlerDataBuffers.vec3d(style.axis.toVector()), - "scale" to ParticleControlerDataBuffers.double(style.scale), - "lastUpdatedGameTime" to ParticleControlerDataBuffers.long(style.lastUpdatedGameTime), - "displayedTime" to ParticleControlerDataBuffers.long(style.displayedTime), - *style.writePacketArgs().map { entry -> entry.key to entry.value }.toTypedArray() - ) - ) - - fun clearAllVisible() { - clientViewStyles.onEach { - it.value.remove() - }.clear() - } - - fun clearServer() { - serverViewStyles.clear() - visible.clear() - } -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt deleted file mode 100644 index acca17be..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/ParticleStyleProvider.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import java.util.UUID - -@Deprecated("使用ParticleComposition") -interface ParticleStyleProvider { - /** - * 创建style - * 执行此方法的时候不要调用基本参数(因为还没初始化) - */ - fun createStyle(uuid: UUID, args: Map>): ParticleGroupStyle -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt deleted file mode 100644 index d8a36a12..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleShapeStyle.kt +++ /dev/null @@ -1,288 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.particles.ParticleDisplayer -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import cn.coostack.cooparticlesapi.utils.builder.PointsBuilder -import cn.coostack.cooparticlesapi.utils.helper.ScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleBezierValueScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleScaleHelper -import cn.coostack.cooparticlesapi.utils.helper.impl.StyleStatusHelper -import net.minecraft.client.particle.ParticleRenderType -import net.minecraft.world.entity.player.Player -import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.TreeMap -import java.util.UUID -import java.util.function.Predicate - -/** - * 为了方便制作子图形作用于父图形 (进行不同方向的旋转, rotate) - * 专门创建此类 - * - * 无法单独生成 - * 继承此类的目的是为这个类创建一个专属形状的子类 - * client属性始终为true - */ -class SequencedParticleShapeStyle(uuid: UUID) : - SequencedParticleStyle(64.0, uuid) { - var scaleHelper: ScaleHelper? = null - - private val displayInvokes = mutableListOf Unit>() - private val beforeDisplayInvokes = - mutableListOf) -> Unit>() - - private val pointBuilders = LinkedHashMap SortedStyleData>() - - /** - * 用于控制粒子的播放顺序 - * 如果满足 animationConditions[animationIndex].first的条件 - * 则会执行 addMultiple(second (second > 0)) - * 或者 removeMultiple(second (second < 0) - * 符合条件后 animationIndex 会递增 - * 直到到达边界 - */ - private val animationConditions = ArrayList, Int>>() - var animationIndex = 0 - private set - - /** - * 通过判断(外层) 提供的状态工具来决定是否反转缩放 - * 在toggleDisplay执行这个方法 - */ - val reverseFunctionFromStatus: (SequencedParticleShapeStyle, StyleStatusHelper) -> Unit = - function@{ it, displayStatus -> - // 必须设置缩放工具 - if (scaleHelper == null) return@function - it.addPreTickAction { - if (it.scaleReversed && it.scaleHelper!!.isZero()) { - it.remove() - return@addPreTickAction - } - if (displayStatus.displayStatus != 2) { - return@addPreTickAction - } - if (!it.scaleReversed) { - it.scaleReversed(false) - } - } - } - var spawnAge = 0 - - /** - * 设置为true时 会利用scaleHelper 每tick增长一点 - */ - var scalePreTick = false - private set - - /** - * 设置为true时 利用scaleHelper 每tick减弱一点 - */ - var scaleReversed = false - private set - - fun appendBuilder( - pointsBuilder: PointsBuilder, - dataBuilder: (RelativeLocation) -> SortedStyleData - ): SequencedParticleShapeStyle { - pointBuilders[pointsBuilder] = dataBuilder - return this - } - - fun appendPoint( - point: RelativeLocation, - dataBuilder: (RelativeLocation) -> SortedStyleData - ): SequencedParticleShapeStyle { - pointBuilders[ - PointsBuilder().also { it.addPoint(point) } - ] = dataBuilder - return this - } - - /** - * @param predicate 设置添加/删除动画的要求 - * @param add 向下的个数 - */ - fun appendAnimateCondition( - predicate: Predicate, - add: Int - ): SequencedParticleShapeStyle { - animationConditions.add(predicate to add) - return this - } - - /** - * @param max 直接让scale从最大开始 - */ - fun scaleReversed(max: Boolean): SequencedParticleShapeStyle { - scaleHelper ?: return this - scaleReversed = true - if (max) { - scaleHelper!!.toggleScale(scaleHelper!!.maxScale) - } - return this - } - - fun loadScaleHelper(minScale: Double, maxScale: Double, scaleTick: Int): SequencedParticleShapeStyle { - scaleHelper = StyleScaleHelper(minScale, maxScale, scaleTick) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - fun loadScaleHelperBezierValue( - minScale: Double, - maxScale: Double, - scaleTick: Int, - c1: RelativeLocation, - c2: RelativeLocation - ): SequencedParticleShapeStyle { - scaleHelper = StyleBezierValueScaleHelper(scaleTick, minScale, maxScale, c1, c2) - scalePreTick = true - scaleHelper!!.loadControler(this) - return this - } - - /** - * 在display之前执行 - */ - fun toggleOnDisplay(toggleMethod: SequencedParticleShapeStyle.() -> Unit): SequencedParticleShapeStyle { - displayInvokes.add(toggleMethod) - return this - } - - /** - * 在生成粒子之前执行 - */ - fun toggleBeforeDisplay(toggleMethod: SequencedParticleShapeStyle.(SortedMap) -> Unit): SequencedParticleShapeStyle { - beforeDisplayInvokes.add(toggleMethod) - return this - } - - - override fun beforeDisplay(styles: SortedMap) { - beforeDisplayInvokes.forEach { - it(styles) - } - } - - - override fun onDisplay() { - displayInvokes.forEach { it() } - addPreTickAction { - if (animationIndex >= animationConditions.size) { - return@addPreTickAction - } - val (predicate, add) = animationConditions[animationIndex] - if (predicate.test(this@SequencedParticleShapeStyle)) { - if (add > 0) { - addMultiple(add) - } else { - removeMultiple(add) - } - animationIndex++ - } - } - addPreTickAction { - spawnAge++ - if (!scalePreTick || scaleHelper == null) { - return@addPreTickAction - } - - if (scaleReversed) { - scaleHelper!!.doScaleReversed() - } else { - scaleHelper!!.doScale() - } - } - } - - private var count = -1 - override fun getParticlesCount(): Int { - // 调用此方法比初始化count早 - if (count == -1 || count == 0) { - count = getCurrentFramesSequenced().size - } - return count - } - - override fun getCurrentFramesSequenced(): SortedMap { - val res = TreeMap() - pointBuilders.forEach { entry -> - res.putAll(entry.key.createWithSequencedStyleData { it, order -> - entry.value(it) - }) - } - return res - } - - override fun writePacketArgsSequenced(): Map> { - return mapOf() - } - - override fun readPacketArgsSequenced(args: Map>) { - } - - - fun fastRotateToPlayerView(player: Player) { - rotateToPoint(RelativeLocation.of(player.forward)) - } - - fun fastStyleData(order: Int, color: Vec3, displayer: (UUID) -> ParticleDisplayer): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - color: Vec3, - sheet: ParticleRenderType, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - color: Vec3, - sheet: ParticleRenderType, - size: Float, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - color: Vec3, - sheet: ParticleRenderType, - size: Float, - alpha: Float, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.colorOfRGB(color.x.toInt(), color.y.toInt(), color.z.toInt()) - this.textureSheet = sheet - this.size = size - this.particleAlpha = alpha - } as SortedStyleData - } - - fun fastStyleData( - order: Int, - sheet: ParticleRenderType, - displayer: (UUID) -> ParticleDisplayer - ): SortedStyleData { - return SortedStyleData(displayer, order).withParticleHandler { - this.textureSheet = sheet - } as SortedStyleData - } -} \ No newline at end of file diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt deleted file mode 100644 index f9825d8c..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/style/SequencedParticleStyle.kt +++ /dev/null @@ -1,551 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.style - -import cn.coostack.cooparticlesapi.CooParticlesConstants -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffer -import cn.coostack.cooparticlesapi.network.buffer.ParticleControlerDataBuffers -import cn.coostack.cooparticlesapi.particles.ControlableParticle -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.impl.ControlableEndRodEffect -import cn.coostack.cooparticlesapi.utils.Math3DUtil -import cn.coostack.cooparticlesapi.utils.MathDataUtil -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.client.multiplayer.ClientLevel -import net.minecraft.world.level.Level -import net.minecraft.world.phys.Vec3 -import java.util.SortedMap -import java.util.UUID -import kotlin.collections.toList -import kotlin.math.PI - - -/** - * 几个免除手动书写 SequencedParticleStyle的方法 - * - * 在这里不建议使用 autoToggle方法 会导致一些奇奇怪怪的问题 (原因未知) 可能是 部分index同步错误导致 - */ -@Deprecated("使用ParticleComposition") -abstract class SequencedParticleStyle(visibleRange: Double = 32.0, uuid: UUID = UUID.randomUUID()) : - ParticleGroupStyle(visibleRange, uuid) { - companion object { - const val CREATE_PARTICLE = 1 - const val DELETE_PARTICLE = 0 - } - - - class SortedStyleData(displayerBuilder: (UUID) -> ParticleDisplayer, val order: Int) : - StyleData(displayerBuilder), Comparable { - override fun compareTo(other: SortedStyleData): Int { - return order - other.order - } - } - - - class SortedStyleDataBuilder { - private var displayerBuilder: (UUID) -> ParticleDisplayer = - { ParticleDisplayer.withSingle(ControlableEndRodEffect(it)) } - private val particleHandlers = mutableListOf Unit>() - private val particleControlerHandlers = mutableListOf Unit>() - fun addParticleHandler( - builder: ControlableParticle.() -> Unit - ): SortedStyleDataBuilder { - particleHandlers.add(builder) - return this - } - - fun addParticleControlerHandler( - builder: ParticleControler.() -> Unit - ): SortedStyleDataBuilder { - particleControlerHandlers.add(builder) - return this - } - - fun clearParticleHandlers(): SortedStyleDataBuilder { - particleHandlers.clear() - return this - } - - fun clearParticleControlers(): SortedStyleDataBuilder { - particleControlerHandlers.clear() - return this - } - - fun removeHandler(index: Int): SortedStyleDataBuilder { - if (index in particleHandlers.indices) particleHandlers.removeAt(index) - return this - } - - fun removeParticleControler(index: Int): SortedStyleDataBuilder { - if (index in particleControlerHandlers.indices) particleControlerHandlers.removeAt(index) - return this - } - - fun displayer(builder: (UUID) -> ParticleDisplayer): SortedStyleDataBuilder { - this.displayerBuilder = builder - return this - } - - fun build(order: Int): SortedStyleData = SortedStyleData(displayerBuilder, order) - .withParticleHandler { - particleHandlers.forEach { it() } - }.withParticleControlerHandler { - particleControlerHandlers.forEach { it() } - } as SortedStyleData - } - - /** - * 在服务器处是0 size的大小 client用于存储某个粒子/粒子组 的展示情况 - * - * 用于彻底解除强制重写的烦恼 - * - * FIXED 修复多余的内存占用 - */ - val displayedStatus: LongArray by lazy { - val count = getParticlesCount() - var page = count / 64 - if (count % 64 > 0) { - page++ - } - LongArray(page) - } - - - /** 统计对应的粒子顺序 (uuid) */ - protected var sequencedParticles = ArrayList>() - private set - - - var displayedParticleCount = 0 - private set(value) { - field = value.coerceAtLeast(0) - } - - /** - * 此参数所指向的状态为false - * - * 服务器可用 不会同步到客户端 - */ - var particleLinkageDisplayCurrentIndex = 0 - private set - - /** - * @return 当前粒子样式的样式个数 (客户端执行getCurrentFramesSequenced返回的集合长度) 请手动计算粒子个数 - * 或者维护一个变量 支持服务器和客户端同时生成 请勿直接在服务器中调用 getCurrentFramesSequenced().size - * 否则会导致服务器崩溃 - */ - abstract fun getParticlesCount(): Int - abstract fun getCurrentFramesSequenced(): SortedMap - abstract fun writePacketArgsSequenced(): Map> - abstract fun readPacketArgsSequenced(args: Map>) - - /** 服务器发包 -> index_status_change -> intArray index, status */ - fun addSingle() { - if (client) { - // 适配在纯客户端页面的添加 - // 由于和服务器无关所以大多数参数无需设置 - if (particleLinkageDisplayCurrentIndex >= getParticlesCount()) { - return - } - toggleFromStatus(particleLinkageDisplayCurrentIndex++, true) - return - } - displayedParticleCount++ - if (getStatus(particleLinkageDisplayCurrentIndex)) { - return - } - setStatus(particleLinkageDisplayCurrentIndex, true) - // 处理服务器 - // 发包 - val args = buildChangeSingleStatusArgs( - particleLinkageDisplayCurrentIndex++, - true - ) - change({}, mapOf(args)) - } - - /** - * 服务器发包 -> indexes_status_change_arg -> intArray indexes - * indexes_status_change_method -> intArray status method - */ - fun addMultiple(count: Int) { - if (client) { - repeat(count) { - addSingle() - } - return - } - if (count <= 0) { - return - } - if (count == 1) { - addSingle() - return - } - displayedParticleCount += count - // 处理服务器 - // 发包 - val indexes = particleLinkageDisplayCurrentIndex..> { - return mapOf( - "indexes_status_change_arg" to ParticleControlerDataBuffers.intArray(indexes), - "indexes_status_change_status" to ParticleControlerDataBuffers.intArray( - intArrayOf(if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount) - ) - ) - } - - private fun buildChangeSingleStatusArgs( - index: Int, - status: Boolean, - ): Pair> { - return "index_status_change" to ParticleControlerDataBuffers.intArray( - intArrayOf( - index, - if (status) CREATE_PARTICLE else DELETE_PARTICLE, displayedParticleCount - ) - ) - } - - open fun beforeDisplay(styles: SortedMap) {} - fun toggleScale(locations: SortedMap) { - super.toggleScale(locations.toMap()) - } - - override fun scale(new: Double) { - if (new < 0.0) { - CooParticlesConstants.logger.error("scale can not be less than zero") - return - } - scale = new - if (displayed) { - toggleScaleDisplayed() - } - } - - override fun display(pos: Vec3, world: Level) { - if (displayed) { - return - } - displayed = true - this.pos = pos - this.world = world - this.client = world.isClientSide - if (!client) { - // 服务器只负责数据同步 不负责粒子生成 - onDisplay() - return - } - flush() - toggleDataStatus() - onDisplay() - } - - /** 清空粒子生成状态 重新生成粒子 */ - override fun flush() { - if (particles.isNotEmpty()) { - clear(true) - clearStatus() - } - displayParticles() - } - - override fun clear(valid: Boolean) { - sequencedParticles.clear() - super.clear(valid) - } - - /** 新的参数用于传输 粒子启用顺序 */ - override fun writePacketArgs(): Map> { - return mapOf( - "status" to ParticleControlerDataBuffers.longArray(displayedStatus), - "displayed_particle_count" to ParticleControlerDataBuffers.int(displayedParticleCount), - *writePacketArgsSequenced().map { it.key to it.value }.toTypedArray() - ) - } - - override fun readPacketArgs(args: Map>) { - args["status"]?.let { - val array = it.loadedValue as LongArray - System.arraycopy(array, 0, displayedStatus, 0, array.size) - } - args["displayed_particle_count"]?.let { displayedParticleCount = it.loadedValue!! as Int } - readPacketArgsSequenced(args) - args["index_status_change"]?.let { - val array = it.loadedValue as IntArray - - /** @see particleLinkageDisplayCurrentIndex */ - val index = array[0] - val status = array[1] == CREATE_PARTICLE - this.displayedParticleCount = array[2] - changeSingleStatus(index, status) - } - - args["indexes_status_change_arg"]?.let { - val indexes = it.loadedValue as IntArray - val methodArray = args["indexes_status_change_status"]!!.loadedValue as IntArray - val status = methodArray[0] == CREATE_PARTICLE - this.displayedParticleCount = methodArray[1] - changeParticlesStatus(indexes, status) - } - } - - fun displayParticles() { - val locations = getCurrentFramesSequenced() - beforeDisplay(locations) - toggleScale(locations) - sequencedParticles.addAll(locations.toList()) - Math3DUtil.rotateAsAxis(locations.values.toList(), axis, rotate) - } - - override fun rotateAsAxis(angle: Double) { - Math3DUtil.rotateAsAxis( - sequencedParticles.map { it.second }.toList(), axis, angle - ) - this.rotate += angle - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client && !autoToggle) { - // 同步到其他客户端 - change( - mapOf( - "rotate_angle" to ParticleControlerDataBuffers.double(angle) - ) - ) - } - } - - override fun rotateToWithAngle(to: RelativeLocation, angle: Double) { - Math3DUtil.rotateAsAxis( - sequencedParticles.map { it.second }.toList(), axis, angle - ) - Math3DUtil.rotatePointsToPoint( - sequencedParticles.map { it.second }.toList(), to, axis - ) - axis = to - this.rotate += angle - if (this.rotate >= 2 * PI) { - this.rotate -= 2 * PI - } - toggleRelative() - if (!client && !autoToggle) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to), - "rotate_angle" to ParticleControlerDataBuffers.double(angle) - ) - ) - } - } - - override fun rotateToPoint(to: RelativeLocation) { - Math3DUtil.rotatePointsToPoint( - sequencedParticles.map { it.second }.toList(), to, axis - ) - axis = to - toggleRelative() - if (!client && !autoToggle) { - // 同步到其他客户端 - change( - mapOf( - "rotate_to" to ParticleControlerDataBuffers.relative(to) - ) - ) - } - } - - final override fun toggleScaleDisplayed() { - sequencedParticles.forEach { - val uuid = it.first.uuid - val len = particleDefaultLength[uuid]!! - val value = it.second - if (len in -1e-3..1e-3) return@forEach - value.multiply(len * scale / value.length()) - } - toggleRelative() - } - - private fun createWithIndex(index: Int) { - if (!client) { - return - } - - if (index !in 0..sequencedParticles.size - 1) { - return - } - - val (data, rl) = sequencedParticles[index] - val uuid = data.uuid - - val displayer = data.displayerBuilder(uuid) - prepareCParticleDisplayer(displayer, sequencedParticles.size) - if (displayer is ParticleDisplayer.SingleParticleDisplayer) { - val controler = ControlParticleManager.createControl(uuid) - controler.applyInitializedAction(data.particleHandler) - } - val toPos = Vec3(pos.x + rl.x, pos.y + rl.y, pos.z + rl.z) - val controler = displayer.display(toPos, world as ClientLevel) ?: return - if (controler is ParticleControler) { - data.particleControlerHandler(controler) - } - registerCParticleNode(controler) - particles[uuid] = controler - particleLocations[controler] = rl - } - - private fun toggleFromStatus(index: Int, status: Boolean) { - if (index >= sequencedParticles.size && client || index > getParticlesCount()) return - if (index < 0) return - if (!client) return - if (status) { - createWithIndex(index) - } else { - val uuid = sequencedParticles[index].first.uuid - val particle = particles[uuid] ?: return - unregisterCParticleNode(particle) - particle.remove() - particles.remove(uuid) - particleLocations.remove(particle) - } - setStatus(index, status) - } - - private fun setStatus(index: Int, status: Boolean) { - val page = MathDataUtil.getStoragePageLong(index) - val container = displayedStatus[page] - val bit = MathDataUtil.getStorageWithBitLong(index) - displayedStatus[page] = MathDataUtil.setStatusLong(container, bit, status) - } - - private fun getStatus(index: Int): Boolean { - val page = MathDataUtil.getStoragePageLong(index) - val container = displayedStatus[page] - val bit = MathDataUtil.getStorageWithBitLong(index) - return MathDataUtil.getStatusLong(container, bit) == 1 - } - - /** 在同步了data状态后 执行生成已经生成的 客户端执行 */ - private fun toggleDataStatus() { - if (!client) { - return - } - - if (displayedStatus.isEmpty()) return - if (!displayed) return - displayedStatus.forEachIndexed { page, container -> - for (bit in 1..64) { - val index = page * 64 + bit - 1 - if (index >= sequencedParticles.size) { - break - } - val status = MathDataUtil.getStatusLong(container, bit) - toggleFromStatus(index, status == 1) - } - - } - } - - private fun clearStatus() { - for (i in displayedStatus.indices) { - displayedStatus[i] = 0L - } - } - - final override fun getCurrentFrames(): Map { - return mapOf() - } - - - final override fun beforeDisplay(styles: Map) {} -} diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt deleted file mode 100644 index 428667b6..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/network/particle/util/ServerParticleUtil.kt +++ /dev/null @@ -1,111 +0,0 @@ -package cn.coostack.cooparticlesapi.network.particle.util - -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleBatchS2C -import cn.coostack.cooparticlesapi.network.packet.server.PacketParticleS2C -import cn.coostack.cooparticlesapi.platform.CooParticlesServices -import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.core.particles.ParticleOptions -import net.minecraft.server.level.ServerLevel -import net.minecraft.world.phys.Vec3 - -object ServerParticleUtil { - - @JvmStatic - fun spawnBatch( - type: ParticleOptions, - world: ServerLevel, - positions: Iterable, - velocity: Vec3, - range: Double, - ) { - val allPositions = positions.toList() - if (allPositions.isEmpty()) return - world.players().forEach { player -> - val visiblePositions = allPositions.filter { player.position().distanceTo(it) <= range } - visiblePositions.chunked(PacketParticleBatchS2C.MAX_PARTICLES).forEach { batch -> - CooParticlesServices.SERVER_NETWORK.send(PacketParticleBatchS2C(type, batch, velocity), player) - } - } - } - - @JvmStatic - fun spawnBatch( - type: ParticleOptions, - world: ServerLevel, - positions: Iterable, - velocity: Vec3, - ) { - positions.toList() - .chunked(PacketParticleBatchS2C.MAX_PARTICLES) - .filter { it.isNotEmpty() } - .forEach { batch -> - val packet = PacketParticleBatchS2C(type, batch, velocity) - world.players().forEach { CooParticlesServices.SERVER_NETWORK.send(packet, it) } - } - } - /** - * 使用minecraft的 spawnParticle方法 - * 可能无法设置粒子移动方向 - */ - fun spawnSingle( - type: ParticleOptions, - world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int - ) { - world.players().forEach { - world.sendParticles( - it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed - ) - } - } - - /** - * 使用minecraft的 spawnParticle方法 - * 可能无法设置粒子移动方向 - */ - fun spawnSingle( - type: ParticleOptions, - world: ServerLevel, pos: Vec3, delta: Vec3, force: Boolean, speed: Double, count: Int, range: Double - ) { - world.players().forEach { - if (it.position().distanceTo(pos) > range) { - return@forEach - } - world.sendParticles( - it, type, force, pos.x, pos.y, pos.z, count, delta.x, delta.y, delta.z, speed - ) - } - } - - /** - * 使用CooParticleAPI的 spawnParticle方法 - */ - fun spawnSingle( - type: ParticleOptions, - world: ServerLevel, - pos: RelativeLocation, - velocity: RelativeLocation, - range: Double - ) { - spawnSingle(type, world, pos.toVector(), velocity.toVector(), range) - } - - fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: RelativeLocation, velocity: RelativeLocation) { - spawnSingle(type, world, pos.toVector(), velocity.toVector()) - } - - fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3, range: Double) { - world.players().forEach { - if (it.position().distanceTo(pos) > range) { - return@forEach - } - CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) - } - } - - fun spawnSingle(type: ParticleOptions, world: ServerLevel, pos: Vec3, velocity: Vec3) { - world.players().forEach { - CooParticlesServices.SERVER_NETWORK.send(PacketParticleS2C(type, pos, velocity), it) - } - } - -} 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..595061e7 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt @@ -0,0 +1,16 @@ +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 net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +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..bb8db76e --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt @@ -0,0 +1,335 @@ +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.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation + +object CooModParticles { + val particleTypes = mutableListOf>>() + val controlableEndRod = register( + "controlable_end_rod", false, { ControlableEndRodEffect.codec }, { ControlableEndRodEffect.packetCode } + ) + + val controlableEnchantment = register( + "controlable_enchantment", + false, + { ControlableEnchantmentEffect.codec }, + { ControlableEnchantmentEffect.packetCode } + ) + + val controlableCloud = register( + "controlable_cloud", false, { ControlableCloudEffect.codec }, { ControlableCloudEffect.packetCode } + ) + + val controlableFlash = register( + "controlable_flash", false, { ControlableFlashEffect.codec }, { ControlableFlashEffect.packetCode } + ) + + val controlableFirework = register( + "controlable_firework", false, { ControlableFireworkEffect.codec }, { ControlableFireworkEffect.packetCode } + ) + + 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 }, { ControlableAngryVillagerEffect.packetCode } + ) + val controlableBubble = register( + "controlable_bubble", false, { ControlableBubbleEffect.codec }, { ControlableBubbleEffect.packetCode } + ) + val controlableBubbleColumnUp = register( + "controlable_bubble_column_up", false, { ControlableBubbleColumnUpEffect.codec }, { ControlableBubbleColumnUpEffect.packetCode } + ) + val controlableBubblePop = register( + "controlable_bubble_pop", false, { ControlableBubblePopEffect.codec }, { ControlableBubblePopEffect.packetCode } + ) + val controlableCampfireCosySmoke = register( + "controlable_campfire_cosy_smoke", true, { ControlableCampfireCosySmokeEffect.codec }, { ControlableCampfireCosySmokeEffect.packetCode } + ) + val controlableCampfireSignalSmoke = register( + "controlable_campfire_signal_smoke", true, { ControlableCampfireSignalSmokeEffect.codec }, { ControlableCampfireSignalSmokeEffect.packetCode } + ) + val controlableComposter = register( + "controlable_composter", false, { ControlableComposterEffect.codec }, { ControlableComposterEffect.packetCode } + ) + val controlableCrit = register( + "controlable_crit", false, { ControlableCritEffect.codec }, { ControlableCritEffect.packetCode } + ) + val controlableCurrentDown = register( + "controlable_current_down", false, { ControlableCurrentDownEffect.codec }, { ControlableCurrentDownEffect.packetCode } + ) + val controlableDamageIndicator = register( + "controlable_damage_indicator", true, { ControlableDamageIndicatorEffect.codec }, { ControlableDamageIndicatorEffect.packetCode } + ) + val controlableDragonBreath = register( + "controlable_dragon_breath", false, { ControlableDragonBreathEffect.codec }, { ControlableDragonBreathEffect.packetCode } + ) + val controlableDolphin = register( + "controlable_dolphin", false, { ControlableDolphinEffect.codec }, { ControlableDolphinEffect.packetCode } + ) + val controlableDrippingLava = register( + "controlable_dripping_lava", false, { ControlableDrippingLavaEffect.codec }, { ControlableDrippingLavaEffect.packetCode } + ) + val controlableFallingLava = register( + "controlable_falling_lava", false, { ControlableFallingLavaEffect.codec }, { ControlableFallingLavaEffect.packetCode } + ) + val controlableLandingLava = register( + "controlable_landing_lava", false, { ControlableLandingLavaEffect.codec }, { ControlableLandingLavaEffect.packetCode } + ) + val controlableDrippingWater = register( + "controlable_dripping_water", false, { ControlableDrippingWaterEffect.codec }, { ControlableDrippingWaterEffect.packetCode } + ) + val controlableFallingWater = register( + "controlable_falling_water", false, { ControlableFallingWaterEffect.codec }, { ControlableFallingWaterEffect.packetCode } + ) + val controlableEffect = register( + "controlable_effect", false, { ControlableEffectParticleEffect.codec }, { ControlableEffectParticleEffect.packetCode } + ) + val controlableEnchantedHit = register( + "controlable_enchanted_hit", false, { ControlableEnchantedHitEffect.codec }, { ControlableEnchantedHitEffect.packetCode } + ) + val controlableExplosion = register( + "controlable_explosion", true, { ControlableExplosionEffect.codec }, { ControlableExplosionEffect.packetCode } + ) + val controlableSonicBoom = register( + "controlable_sonic_boom", true, { ControlableSonicBoomEffect.codec }, { ControlableSonicBoomEffect.packetCode } + ) + val controlableGust = register( + "controlable_gust", true, { ControlableGustEffect.codec }, { ControlableGustEffect.packetCode } + ) + val controlableSmallGust = register( + "controlable_small_gust", false, { ControlableSmallGustEffect.codec }, { ControlableSmallGustEffect.packetCode } + ) + val controlableFishing = register( + "controlable_fishing", false, { ControlableFishingEffect.codec }, { ControlableFishingEffect.packetCode } + ) + val controlableFlame = register( + "controlable_flame", false, { ControlableFlameEffect.codec }, { ControlableFlameEffect.packetCode } + ) + val controlableInfested = register( + "controlable_infested", false, { ControlableInfestedEffect.codec }, { ControlableInfestedEffect.packetCode } + ) + val controlableCherryLeaves = register( + "controlable_cherry_leaves", false, { ControlableCherryLeavesEffect.codec }, { ControlableCherryLeavesEffect.packetCode } + ) + val controlableSculkSoul = register( + "controlable_sculk_soul", false, { ControlableSculkSoulEffect.codec }, { ControlableSculkSoulEffect.packetCode } + ) + val controlableSculkChargePop = register( + "controlable_sculk_charge_pop", true, { ControlableSculkChargePopEffect.codec }, { ControlableSculkChargePopEffect.packetCode } + ) + val controlableSoul = register( + "controlable_soul", false, { ControlableSoulEffect.codec }, { ControlableSoulEffect.packetCode } + ) + val controlableSoulFireFlame = register( + "controlable_soul_fire_flame", false, { ControlableSoulFireFlameEffect.codec }, { ControlableSoulFireFlameEffect.packetCode } + ) + val controlableHappyVillager = register( + "controlable_happy_villager", false, { ControlableHappyVillagerEffect.codec }, { ControlableHappyVillagerEffect.packetCode } + ) + val controlableHeart = register( + "controlable_heart", false, { ControlableHeartEffect.codec }, { ControlableHeartEffect.packetCode } + ) + val controlableInstantEffect = register( + "controlable_instant_effect", false, { ControlableInstantEffectParticleEffect.codec }, { ControlableInstantEffectParticleEffect.packetCode } + ) + val controlableLargeSmoke = register( + "controlable_large_smoke", false, { ControlableLargeSmokeEffect.codec }, { ControlableLargeSmokeEffect.packetCode } + ) + val controlableLava = register( + "controlable_lava", false, { ControlableLavaEffect.codec }, { ControlableLavaEffect.packetCode } + ) + val controlableMycelium = register( + "controlable_mycelium", false, { ControlableMyceliumEffect.codec }, { ControlableMyceliumEffect.packetCode } + ) + val controlableNautilus = register( + "controlable_nautilus", false, { ControlableNautilusEffect.codec }, { ControlableNautilusEffect.packetCode } + ) + val controlableNote = register( + "controlable_note", false, { ControlableNoteEffect.codec }, { ControlableNoteEffect.packetCode } + ) + val controlablePoof = register( + "controlable_poof", true, { ControlablePoofEffect.codec }, { ControlablePoofEffect.packetCode } + ) + val controlablePortal = register( + "controlable_portal", false, { ControlablePortalEffect.codec }, { ControlablePortalEffect.packetCode } + ) + val controlableRain = register( + "controlable_rain", false, { ControlableRainEffect.codec }, { ControlableRainEffect.packetCode } + ) + val controlableSmoke = register( + "controlable_smoke", false, { ControlableSmokeEffect.codec }, { ControlableSmokeEffect.packetCode } + ) + val controlableWhiteSmoke = register( + "controlable_white_smoke", false, { ControlableWhiteSmokeEffect.codec }, { ControlableWhiteSmokeEffect.packetCode } + ) + val controlableSneeze = register( + "controlable_sneeze", false, { ControlableSneezeEffect.codec }, { ControlableSneezeEffect.packetCode } + ) + val controlableSnowflake = register( + "controlable_snowflake", false, { ControlableSnowflakeEffect.codec }, { ControlableSnowflakeEffect.packetCode } + ) + val controlableSpit = register( + "controlable_spit", true, { ControlableSpitEffect.codec }, { ControlableSpitEffect.packetCode } + ) + val controlableSweepAttack = register( + "controlable_sweep_attack", true, { ControlableSweepAttackEffect.codec }, { ControlableSweepAttackEffect.packetCode } + ) + val controlableTotemOfUndying = register( + "controlable_totem_of_undying", false, { ControlableTotemOfUndyingEffect.codec }, { ControlableTotemOfUndyingEffect.packetCode } + ) + val controlableSquidInk = register( + "controlable_squid_ink", true, { ControlableSquidInkEffect.codec }, { ControlableSquidInkEffect.packetCode } + ) + val controlableUnderwater = register( + "controlable_underwater", false, { ControlableUnderwaterEffect.codec }, { ControlableUnderwaterEffect.packetCode } + ) + val controlableWitch = register( + "controlable_witch", false, { ControlableWitchEffect.codec }, { ControlableWitchEffect.packetCode } + ) + val controlableDrippingHoney = register( + "controlable_dripping_honey", false, { ControlableDrippingHoneyEffect.codec }, { ControlableDrippingHoneyEffect.packetCode } + ) + val controlableFallingHoney = register( + "controlable_falling_honey", false, { ControlableFallingHoneyEffect.codec }, { ControlableFallingHoneyEffect.packetCode } + ) + val controlableLandingHoney = register( + "controlable_landing_honey", false, { ControlableLandingHoneyEffect.codec }, { ControlableLandingHoneyEffect.packetCode } + ) + val controlableFallingNectar = register( + "controlable_falling_nectar", false, { ControlableFallingNectarEffect.codec }, { ControlableFallingNectarEffect.packetCode } + ) + val controlableFallingSporeBlossom = register( + "controlable_falling_spore_blossom", false, { ControlableFallingSporeBlossomEffect.codec }, { ControlableFallingSporeBlossomEffect.packetCode } + ) + val controlableSporeBlossomAir = register( + "controlable_spore_blossom_air", false, { ControlableSporeBlossomAirEffect.codec }, { ControlableSporeBlossomAirEffect.packetCode } + ) + val controlableAsh = register( + "controlable_ash", false, { ControlableAshEffect.codec }, { ControlableAshEffect.packetCode } + ) + val controlableCrimsonSpore = register( + "controlable_crimson_spore", false, { ControlableCrimsonSporeEffect.codec }, { ControlableCrimsonSporeEffect.packetCode } + ) + val controlableWarpedSpore = register( + "controlable_warped_spore", false, { ControlableWarpedSporeEffect.codec }, { ControlableWarpedSporeEffect.packetCode } + ) + val controlableDrippingObsidianTear = register( + "controlable_dripping_obsidian_tear", false, { ControlableDrippingObsidianTearEffect.codec }, { ControlableDrippingObsidianTearEffect.packetCode } + ) + val controlableFallingObsidianTear = register( + "controlable_falling_obsidian_tear", false, { ControlableFallingObsidianTearEffect.codec }, { ControlableFallingObsidianTearEffect.packetCode } + ) + val controlableLandingObsidianTear = register( + "controlable_landing_obsidian_tear", false, { ControlableLandingObsidianTearEffect.codec }, { ControlableLandingObsidianTearEffect.packetCode } + ) + val controlableReversePortal = register( + "controlable_reverse_portal", false, { ControlableReversePortalEffect.codec }, { ControlableReversePortalEffect.packetCode } + ) + val controlableWhiteAsh = register( + "controlable_white_ash", false, { ControlableWhiteAshEffect.codec }, { ControlableWhiteAshEffect.packetCode } + ) + val controlableSmallFlame = register( + "controlable_small_flame", false, { ControlableSmallFlameEffect.codec }, { ControlableSmallFlameEffect.packetCode } + ) + val controlableDrippingDripstoneWater = register( + "controlable_dripping_dripstone_water", false, { ControlableDrippingDripstoneWaterEffect.codec }, { ControlableDrippingDripstoneWaterEffect.packetCode } + ) + val controlableFallingDripstoneWater = register( + "controlable_falling_dripstone_water", false, { ControlableFallingDripstoneWaterEffect.codec }, { ControlableFallingDripstoneWaterEffect.packetCode } + ) + val controlableDrippingDripstoneLava = register( + "controlable_dripping_dripstone_lava", false, { ControlableDrippingDripstoneLavaEffect.codec }, { ControlableDrippingDripstoneLavaEffect.packetCode } + ) + val controlableFallingDripstoneLava = register( + "controlable_falling_dripstone_lava", false, { ControlableFallingDripstoneLavaEffect.codec }, { ControlableFallingDripstoneLavaEffect.packetCode } + ) + val controlableGlowSquidInk = register( + "controlable_glow_squid_ink", true, { ControlableGlowSquidInkEffect.codec }, { ControlableGlowSquidInkEffect.packetCode } + ) + val controlableGlow = register( + "controlable_glow", true, { ControlableGlowEffect.codec }, { ControlableGlowEffect.packetCode } + ) + val controlableWaxOn = register( + "controlable_wax_on", true, { ControlableWaxOnEffect.codec }, { ControlableWaxOnEffect.packetCode } + ) + val controlableWaxOff = register( + "controlable_wax_off", true, { ControlableWaxOffEffect.codec }, { ControlableWaxOffEffect.packetCode } + ) + val controlableElectricSpark = register( + "controlable_electric_spark", true, { ControlableElectricSparkEffect.codec }, { ControlableElectricSparkEffect.packetCode } + ) + val controlableScrape = register( + "controlable_scrape", true, { ControlableScrapeEffect.codec }, { ControlableScrapeEffect.packetCode } + ) + val controlableEggCrack = register( + "controlable_egg_crack", false, { ControlableEggCrackEffect.codec }, { ControlableEggCrackEffect.packetCode } + ) + val controlableDustPlume = register( + "controlable_dust_plume", false, { ControlableDustPlumeEffect.codec }, { ControlableDustPlumeEffect.packetCode } + ) + val controlableTrialSpawnerDetection = register( + "controlable_trial_spawner_detection", true, { ControlableTrialSpawnerDetectionEffect.codec }, { ControlableTrialSpawnerDetectionEffect.packetCode } + ) + val controlableTrialSpawnerDetectionOminous = register( + "controlable_trial_spawner_detection_ominous", true, { ControlableTrialSpawnerDetectionOminousEffect.codec }, { ControlableTrialSpawnerDetectionOminousEffect.packetCode } + ) + val controlableVaultConnection = register( + "controlable_vault_connection", true, { ControlableVaultConnectionEffect.codec }, { ControlableVaultConnectionEffect.packetCode } + ) + val controlableRaidOmen = register( + "controlable_raid_omen", false, { ControlableRaidOmenEffect.codec }, { ControlableRaidOmenEffect.packetCode } + ) + val controlableTrialOmen = register( + "controlable_trial_omen", false, { ControlableTrialOmenEffect.codec }, { ControlableTrialOmenEffect.packetCode } + ) + val controlableOminousSpawning = register( + "controlable_ominous_spawning", true, { ControlableOminousSpawningEffect.codec }, { ControlableOminousSpawningEffect.packetCode } + ) + + fun reg() { + } + + fun register( + id: String, alwaysShow: Boolean, + codecGetter: (type: ParticleType) -> MapCodec, + packetCodec: (type: ParticleType) -> ForgeStreamCodec, + ): CommonDeferredRegistry> { + val registry = CommonDeferredRegistry( + BuiltInRegistries.PARTICLE_TYPE, + ResourceLocation.fromNamespaceAndPath(CooParticlesConstants.MOD_ID, id) + ) { + object : ParticleType(alwaysShow) { + override fun codec(): MapCodec { + return codecGetter(this) + } + + override fun streamCodec(): StreamCodec { + return packetCodec(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/SimpleControlableParticleEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt new file mode 100644 index 00000000..9525b328 --- /dev/null +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt @@ -0,0 +1,68 @@ +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 net.minecraft.network.FriendlyByteBuf +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/ClientNetworking.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt deleted file mode 100644 index a52de482..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ClientNetworking.kt +++ /dev/null @@ -1,5 +0,0 @@ -package cn.coostack.cooparticlesapi.platform - -interface ClientNetworking { - fun send(packet: Any) -} 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 index 545d1dd8..5ae2f74c 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ForgeNetworkChannel.kt @@ -1,16 +1,17 @@ package cn.coostack.cooparticlesapi.platform -import cn.coostack.cooparticlesapi.network.packet.api.CooPacket import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeC2S import cn.coostack.cooparticlesapi.network.packet.api.envelope.CooPacketEnvelopeS2C -import net.minecraft.network.PacketByteBuf 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 -class ForgeNetworkChannel(private val modId: String) { +object ForgeNetworkChannel { val channel: SimpleChannel = net.minecraftforge.network.NetworkRegistry.newSimpleChannel( - ResourceLocation(modId, "main"), + ResourceLocation(CooParticlesConstants.MOD_ID, "main"), { true }, { true }, { true } @@ -41,16 +42,17 @@ class ForgeNetworkChannel(private val modId: String) { ) } - fun sendTo(player: net.minecraft.server.level.ServerPlayer, packet: CooPacket) { - channel.sendTo(player, packet) + fun sendEnvelopeS2CTo(envelope: CooPacketEnvelopeS2C, player: ServerPlayer) { + channel.sendTo(player, envelope) } - fun sendToAll(packet: CooPacket) { - channel.sendToAll(packet) + fun sendEnvelopeS2CToAll(envelope: CooPacketEnvelopeS2C) { + channel.sendToAll(envelope) } - fun sendToServer(packet: CooPacket) { - channel.sendToServer(packet) + 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) { 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/ServerNetworking.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt deleted file mode 100644 index 984884a9..00000000 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/platform/ServerNetworking.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cn.coostack.cooparticlesapi.platform - -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.level.ChunkPos - -interface ServerNetworking { - fun send(packet: Any, to: ServerPlayer) - fun sendAllPlayers(packet: Any) - fun sendToPlayersTrackingChunk(world: ServerLevel, chunk: ChunkPos, packet: Any) - - fun sendToWorld(world: ServerLevel, packet: Any) { - world.players().forEach { send(packet, it) } - } -} 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..8a4fb318 --- /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.FriendlyByteBuf +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..55db737a --- /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.FriendlyByteBuf +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: FriendlyByteBuf, instance: RenderEntity) { + instance.uuid = buf.readUUID() + instance.pos = buf.readVec3() + instance.canceled = buf.readBoolean() + instance.age = buf.readInt() + instance.dirty = false + } + + fun encodeBase(buf: FriendlyByteBuf, entity: RenderEntity) { + buf.writeUUID(entity.uuid) + buf.writeVec3(entity.pos) + buf.writeBoolean(entity.canceled) + buf.writeInt(entity.age) + } + + fun createCodec( + factory: () -> T, + encodeExtra: (FriendlyByteBuf, T) -> Unit = { _, _ -> }, + decodeExtra: (FriendlyByteBuf, 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 = FriendlyByteBuf(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/PostEffectParams.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt new file mode 100644 index 00000000..f37e5507 --- /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.FriendlyByteBuf +import net.minecraft.resources.ResourceLocation + +internal sealed interface PostEffectParamValue { + fun write(buf: FriendlyByteBuf) + + data class BoolValue(val value: Boolean) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeBoolean(value) + } + } + + data class IntValue(val value: Int) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeInt(value) + } + } + + data class LongValue(val value: Long) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeLong(value) + } + } + + data class FloatValue(val value: Float) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeFloat(value) + } + } + + data class DoubleValue(val value: Double) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeDouble(value) + } + } + + data class StringValue(val value: String) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeUtf(value) + } + } + + data class ResourceValue(val value: ResourceLocation) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeResourceLocation(value) + } + } + + data class Vec2Value(val x: Float, val y: Float) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + buf.writeFloat(x) + buf.writeFloat(y) + } + } + + data class Vec3Value(val x: Double, val y: Double, val z: Double) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + 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: FriendlyByteBuf) { + buf.writeFloat(red) + buf.writeFloat(green) + buf.writeFloat(blue) + buf.writeFloat(alpha) + } + } + + data class UniformValue(val value: CooUniformValue) : PostEffectParamValue { + override fun write(buf: FriendlyByteBuf) { + CooUniformValue.STREAM_CODEC.encode(buf, value) + } + } + + companion object { + fun writeTyped(buf: FriendlyByteBuf, value: PostEffectParamValue) { + buf.writeUtf(value.typeId) + value.write(buf) + } + + fun readTyped(buf: FriendlyByteBuf): 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: FriendlyByteBuf) { + buf.writeInt(values.size) + values.toSortedMap().forEach { (name, value) -> + buf.writeUtf(name) + PostEffectParamValue.writeTyped(buf, value) + } + } + + companion object { + val EMPTY = PostEffectParams() + + fun read(buf: FriendlyByteBuf): 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..ce833e94 --- /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.FriendlyByteBuf +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..4a2692cc --- /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.FriendlyByteBuf + +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..eef40634 --- /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.FriendlyByteBuf +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..094e58c8 --- /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.FriendlyByteBuf +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: FriendlyByteBuf) { + 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: FriendlyByteBuf): 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..16c865e9 --- /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 +import net.minecraft.network.FriendlyByteBuf + +class InterpolatorDouble(value: Double) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of( + { 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..ff08d6b4 --- /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 +import net.minecraft.network.FriendlyByteBuf + +class InterpolatorFloat(value: Float) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of( + { 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..704d4cdf --- /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 net.minecraft.network.FriendlyByteBuf +import org.joml.Quaternionf + +class InterpolatorQuaternionf(value: Quaternionf) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of( + { 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..388a8768 --- /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.network.FriendlyByteBuf +import net.minecraft.world.phys.Vec3 + +class InterpolatorRelativeLocation(value: RelativeLocation) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of( + { 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..17327b5d --- /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.network.FriendlyByteBuf +import net.minecraft.world.phys.Vec3 + +class InterpolatorVec3d(value: Vec3) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of( + { 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..1d308c41 --- /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 net.minecraft.network.FriendlyByteBuf +import org.joml.Vector3f + +class InterpolatorVector3f(value: Vector3f) : AbstractInterpolatorData(value) { + + companion object { + @JvmStatic + val CODEC = ForgeStreamCodec.of( + { 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/settings.gradle b/settings.gradle index 8af99515..589f3402 100644 --- a/settings.gradle +++ b/settings.gradle @@ -51,4 +51,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 From b25fd392bf6b3d21e37adf8731ded02f597f6258 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 21:07:29 +0800 Subject: [PATCH 18/41] fix: continue removing MC 1.20.5+ APIs from Forge module --- .../data/holder/DataHolderManager.kt | 10 +- .../api/controler/SerializableData.kt | 3 - .../cparticle/CParticleColorCurve.kt | 2 +- .../data/holder/DataHolderManager.kt | 9 +- .../display/AutoDisplayEntity.kt | 2 +- .../cooparticlesapi/display/DisplayEntity.kt | 6 +- .../display/DisplayEntityManager.kt | 4 +- .../manager/ParticleCompositionManager.kt | 5 +- .../network/particle/emitters/AutoEmitters.kt | 2 - .../particle/emitters/AutoParticleEmitters.kt | 2 - .../AutoTransformableCParticleEmitter.kt | 2 - .../particle/emitters/ClassEmitters.kt | 6 +- .../emitters/ClassParticleEmitters.kt | 6 +- .../emitters/CompositionEmittersData.kt | 3 +- .../emitters/ControlableCParticleData.kt | 2 - .../emitters/DisplayEntityEmittersData.kt | 3 +- .../emitters/ParticleEmittersManager.kt | 6 +- .../emitters/SimpleRandomParticleData.kt | 3 +- .../emitters/TransformableCParticleEmitter.kt | 2 +- .../particles/ControlableParticleEffect.kt | 2 - .../particles/CooModParticles.kt | 189 +++++++++--------- .../impl/SimpleControlableParticleEffect.kt | 1 - .../renderer/AutoRenderEntity.kt | 2 +- .../cooparticlesapi/renderer/RenderEntity.kt | 12 +- .../renderer/post/PostEffectParams.kt | 34 ++-- .../runtime/ClientRenderEntityRegistry.kt | 2 +- .../runtime/ClientRenderEntityType.kt | 2 +- .../runtime/RenderEntityAutoRegistry.kt | 2 +- .../terrain/CooTerrainMappingRegion.kt | 6 +- .../interpolator/data/InterpolatorDouble.kt | 4 +- .../interpolator/data/InterpolatorFloat.kt | 4 +- .../data/InterpolatorQuaternionf.kt | 4 +- .../data/InterpolatorRelativeLocation.kt | 4 +- .../interpolator/data/InterpolatorVec3d.kt | 4 +- .../interpolator/data/InterpolatorVector3f.kt | 4 +- 35 files changed, 163 insertions(+), 191 deletions(-) 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/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt index 24aab070..fadb635d 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/api/controler/SerializableData.kt @@ -3,9 +3,6 @@ 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.network.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import net.minecraft.world.phys.Vec3 interface SerializableData { diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt index 26d768e9..6f3d99e0 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/cparticle/CParticleColorCurve.kt @@ -1,7 +1,7 @@ package cn.coostack.cooparticlesapi.cparticle import net.minecraft.network.PacketByteBuf -import net.minecraft.network.codec.StreamCodec + import org.joml.Vector3f import org.joml.Vector3fc 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 index 3b1307cf..a26196b7 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/data/holder/DataHolderManager.kt @@ -6,13 +6,12 @@ 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.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 +32,11 @@ object DataHolderManager { registeredTypes[randomInstance::class.java.name] = codec } - fun register(type: Class<*>, codec: StreamCodec<*, *>) { + fun register(type: Class<*>, codec: cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec<*>) { registeredTypes[type.name] = codec } - fun getCodecFromID(id: String): StreamCodec<*, *>? { + fun getCodecFromID(id: String): cn.coostack.cooparticlesapi.annotations.codec.CommonStreamCodec<*>? { return registeredTypes[id] ?: CodecHelper.supposedTypes[id] } @@ -63,7 +62,7 @@ object DataHolderManager { register(instance) } - private fun findCodec(instance: Any): StreamCodec<*, *> { + 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}") diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt index 4900f8be..b4d831ed 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/AutoDisplayEntity.kt @@ -1,7 +1,7 @@ package cn.coostack.cooparticlesapi.display import cn.coostack.cooparticlesapi.annotations.display.handle.DisplayEntityRegistryHelper -import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt index 26061ce8..21c82c79 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntity.kt @@ -12,7 +12,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 import org.joml.Matrix4f @@ -24,7 +24,7 @@ abstract class DisplayEntity( var world: Level? ) : Controlable, ServerControler, Tickable, NetworkDirtyMarkable { companion object { - fun encodeBase(data: DisplayEntity, buf: FriendlyByteBuf) { + fun encodeBase(data: DisplayEntity, buf: PacketByteBuf) { buf.writeVec3(data.pos) buf.writeFloat(data.yaw) buf.writeFloat(data.pitch) @@ -34,7 +34,7 @@ abstract class DisplayEntity( buf.writeUUID(data.controlUUID) } - fun decodeBase(instance: DisplayEntity, buf: FriendlyByteBuf) { + fun decodeBase(instance: DisplayEntity, buf: PacketByteBuf) { instance.apply { pos = buf.readVec3() yaw = buf.readFloat() diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt index 9ba99a4d..adb6dc27 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/display/DisplayEntityManager.kt @@ -14,7 +14,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.server.level.ServerPlayer import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 @@ -206,7 +206,7 @@ object DisplayEntityManager { val registryAccess = CooParticlesAPI.registryAccessOrNull ?: return val uuid = entity.controlUUID val type = entity::class.java.name - val buf = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) + val buf = PacketByteBuf(Unpooled.buffer()) val data = try { entity.getCodec().encode(buf, entity) ByteArray(buf.readableBytes()).also { buf.readBytes(it) } 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 index b1626208..635c52ef 100644 --- 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 @@ -12,8 +12,7 @@ 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.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.player.Player import java.lang.ref.ReferenceQueue @@ -185,7 +184,7 @@ object ParticleCompositionManager { } val registryAccess = CooParticlesAPI.registryAccessOrNull ?: return val type = composition::class.java.name - val buf = RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess) + val buf = PacketByteBuf(Unpooled.buffer()) val data = try { registeredTypes[type]!!.encode(buf, composition) ByteArray(buf.readableBytes()).also { buf.readBytes(it) } 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 index f50c843f..ec222740 100644 --- 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 @@ -3,8 +3,6 @@ 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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 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 index face5664..1db5725b 100644 --- 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 @@ -3,8 +3,6 @@ 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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 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 index 8932d8e4..fd7eba4d 100644 --- 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 @@ -3,8 +3,6 @@ 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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 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 index 8bc94c29..1309263f 100644 --- 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 @@ -21,7 +21,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.BlockHitResult import net.minecraft.world.phys.Vec3 @@ -87,7 +87,7 @@ abstract class ClassEmitters( } companion object { - fun encodeBase(data: ClassEmitters, buf: FriendlyByteBuf) { + fun encodeBase(data: ClassEmitters, buf: PacketByteBuf) { val handles = data.collectEventHandles() buf.writeInt(handles.size) handles.forEach { @@ -109,7 +109,7 @@ abstract class ClassEmitters( data.wind.getCodec().encode(buf, data.wind) } - fun decodeBase(container: ClassEmitters, buf: FriendlyByteBuf) { + fun decodeBase(container: ClassEmitters, buf: PacketByteBuf) { val handlerCount = buf.readInt() val handlers = ArrayList() repeat(handlerCount) { 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 index d815def9..bff348d4 100644 --- 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 @@ -24,7 +24,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.entity.Entity import net.minecraft.world.level.Level import net.minecraft.world.phys.BlockHitResult @@ -94,7 +94,7 @@ abstract class ClassParticleEmitters( } companion object { - fun encodeBase(data: ClassParticleEmitters, buf: FriendlyByteBuf) { + fun encodeBase(data: ClassParticleEmitters, buf: PacketByteBuf) { val handles = data.collectEventHandles() buf.writeInt(handles.size) handles.forEach { @@ -117,7 +117,7 @@ abstract class ClassParticleEmitters( data.wind.getCodec().encode(buf, data.wind) } - fun decodeBase(container: ClassParticleEmitters, buf: FriendlyByteBuf) { + fun decodeBase(container: ClassParticleEmitters, buf: PacketByteBuf) { val handlerCount = buf.readInt() val handlerList = ArrayList() repeat(handlerCount) { 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 index ed098cef..8a53bd0c 100644 --- 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 @@ -1,8 +1,7 @@ package cn.coostack.cooparticlesapi.network.particle.emitters import cn.coostack.cooparticlesapi.network.particle.composition.ParticleComposition -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.PacketByteBuf open class CompositionEmittersData : ControlableParticleData() { companion object { 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 index b08d519f..d223548f 100644 --- 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 @@ -4,8 +4,6 @@ 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 net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import org.joml.Vector3f open class ControlableCParticleData : ControlableParticleData() { 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 index 0198ff91..a2029366 100644 --- 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 @@ -1,8 +1,7 @@ package cn.coostack.cooparticlesapi.network.particle.emitters import cn.coostack.cooparticlesapi.display.DisplayEntity -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.PacketByteBuf open class DisplayEntityEmittersData : ControlableParticleData() { companion object { 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 index 3f1b07ab..0ef07350 100644 --- 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 @@ -14,8 +14,6 @@ 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.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.player.Player import net.minecraft.world.level.Level @@ -294,10 +292,8 @@ object ParticleEmittersManager { private fun encodeEmittersToArray(emitters: ParticleEmitters): ByteArray { val codec = emitters.getCodec() - val registryAccess = emitters.world?.registryAccess() ?: CooParticlesAPI.registryAccessOrNull ?: return ByteArray(0) - val buf = RegistryFriendlyByteBuf( + val buf = PacketByteBuf( Unpooled.buffer(), - registryAccess ) return try { codec.encode(buf, emitters) 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 index 03fa7ba1..157745d3 100644 --- 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 @@ -3,8 +3,7 @@ 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.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.PacketByteBuf import org.joml.Vector3f import kotlin.math.roundToInt import kotlin.random.Random 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 index 9e1a8008..32ebd3d3 100644 --- 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 @@ -17,7 +17,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 import org.joml.Quaternionf diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt index 595061e7..298e6fb5 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/ControlableParticleEffect.kt @@ -6,8 +6,6 @@ import com.mojang.serialization.MapCodec import com.mojang.serialization.codecs.RecordCodecBuilder import io.netty.buffer.Unpooled import net.minecraft.core.particles.ParticleOptions -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.network.codec.StreamCodec import java.util.UUID abstract class ControlableParticleEffect(var controlUUID: UUID, val faceToPlayer: Boolean = true) : ParticleOptions { diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt index bb8db76e..0b7abac2 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/CooModParticles.kt @@ -7,15 +7,15 @@ 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.network.FriendlyByteBuf -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec + + + import net.minecraft.resources.ResourceLocation object CooModParticles { val particleTypes = mutableListOf>>() val controlableEndRod = register( - "controlable_end_rod", false, { ControlableEndRodEffect.codec }, { ControlableEndRodEffect.packetCode } + "controlable_end_rod", false, { ControlableEndRodEffect.codec } ) val controlableEnchantment = register( @@ -26,15 +26,15 @@ object CooModParticles { ) val controlableCloud = register( - "controlable_cloud", false, { ControlableCloudEffect.codec }, { ControlableCloudEffect.packetCode } + "controlable_cloud", false, { ControlableCloudEffect.codec } ) val controlableFlash = register( - "controlable_flash", false, { ControlableFlashEffect.codec }, { ControlableFlashEffect.packetCode } + "controlable_flash", false, { ControlableFlashEffect.codec } ) val controlableFirework = register( - "controlable_firework", false, { ControlableFireworkEffect.codec }, { ControlableFireworkEffect.packetCode } + "controlable_firework", false, { ControlableFireworkEffect.codec } ) val controlableFallingDust = register( @@ -52,259 +52,259 @@ object CooModParticles { ) val controlableAngryVillager = register( - "controlable_angry_villager", false, { ControlableAngryVillagerEffect.codec }, { ControlableAngryVillagerEffect.packetCode } + "controlable_angry_villager", false, { ControlableAngryVillagerEffect.codec } ) val controlableBubble = register( - "controlable_bubble", false, { ControlableBubbleEffect.codec }, { ControlableBubbleEffect.packetCode } + "controlable_bubble", false, { ControlableBubbleEffect.codec } ) val controlableBubbleColumnUp = register( - "controlable_bubble_column_up", false, { ControlableBubbleColumnUpEffect.codec }, { ControlableBubbleColumnUpEffect.packetCode } + "controlable_bubble_column_up", false, { ControlableBubbleColumnUpEffect.codec } ) val controlableBubblePop = register( - "controlable_bubble_pop", false, { ControlableBubblePopEffect.codec }, { ControlableBubblePopEffect.packetCode } + "controlable_bubble_pop", false, { ControlableBubblePopEffect.codec } ) val controlableCampfireCosySmoke = register( - "controlable_campfire_cosy_smoke", true, { ControlableCampfireCosySmokeEffect.codec }, { ControlableCampfireCosySmokeEffect.packetCode } + "controlable_campfire_cosy_smoke", true, { ControlableCampfireCosySmokeEffect.codec } ) val controlableCampfireSignalSmoke = register( - "controlable_campfire_signal_smoke", true, { ControlableCampfireSignalSmokeEffect.codec }, { ControlableCampfireSignalSmokeEffect.packetCode } + "controlable_campfire_signal_smoke", true, { ControlableCampfireSignalSmokeEffect.codec } ) val controlableComposter = register( - "controlable_composter", false, { ControlableComposterEffect.codec }, { ControlableComposterEffect.packetCode } + "controlable_composter", false, { ControlableComposterEffect.codec } ) val controlableCrit = register( - "controlable_crit", false, { ControlableCritEffect.codec }, { ControlableCritEffect.packetCode } + "controlable_crit", false, { ControlableCritEffect.codec } ) val controlableCurrentDown = register( - "controlable_current_down", false, { ControlableCurrentDownEffect.codec }, { ControlableCurrentDownEffect.packetCode } + "controlable_current_down", false, { ControlableCurrentDownEffect.codec } ) val controlableDamageIndicator = register( - "controlable_damage_indicator", true, { ControlableDamageIndicatorEffect.codec }, { ControlableDamageIndicatorEffect.packetCode } + "controlable_damage_indicator", true, { ControlableDamageIndicatorEffect.codec } ) val controlableDragonBreath = register( - "controlable_dragon_breath", false, { ControlableDragonBreathEffect.codec }, { ControlableDragonBreathEffect.packetCode } + "controlable_dragon_breath", false, { ControlableDragonBreathEffect.codec } ) val controlableDolphin = register( - "controlable_dolphin", false, { ControlableDolphinEffect.codec }, { ControlableDolphinEffect.packetCode } + "controlable_dolphin", false, { ControlableDolphinEffect.codec } ) val controlableDrippingLava = register( - "controlable_dripping_lava", false, { ControlableDrippingLavaEffect.codec }, { ControlableDrippingLavaEffect.packetCode } + "controlable_dripping_lava", false, { ControlableDrippingLavaEffect.codec } ) val controlableFallingLava = register( - "controlable_falling_lava", false, { ControlableFallingLavaEffect.codec }, { ControlableFallingLavaEffect.packetCode } + "controlable_falling_lava", false, { ControlableFallingLavaEffect.codec } ) val controlableLandingLava = register( - "controlable_landing_lava", false, { ControlableLandingLavaEffect.codec }, { ControlableLandingLavaEffect.packetCode } + "controlable_landing_lava", false, { ControlableLandingLavaEffect.codec } ) val controlableDrippingWater = register( - "controlable_dripping_water", false, { ControlableDrippingWaterEffect.codec }, { ControlableDrippingWaterEffect.packetCode } + "controlable_dripping_water", false, { ControlableDrippingWaterEffect.codec } ) val controlableFallingWater = register( - "controlable_falling_water", false, { ControlableFallingWaterEffect.codec }, { ControlableFallingWaterEffect.packetCode } + "controlable_falling_water", false, { ControlableFallingWaterEffect.codec } ) val controlableEffect = register( - "controlable_effect", false, { ControlableEffectParticleEffect.codec }, { ControlableEffectParticleEffect.packetCode } + "controlable_effect", false, { ControlableEffectParticleEffect.codec } ) val controlableEnchantedHit = register( - "controlable_enchanted_hit", false, { ControlableEnchantedHitEffect.codec }, { ControlableEnchantedHitEffect.packetCode } + "controlable_enchanted_hit", false, { ControlableEnchantedHitEffect.codec } ) val controlableExplosion = register( - "controlable_explosion", true, { ControlableExplosionEffect.codec }, { ControlableExplosionEffect.packetCode } + "controlable_explosion", true, { ControlableExplosionEffect.codec } ) val controlableSonicBoom = register( - "controlable_sonic_boom", true, { ControlableSonicBoomEffect.codec }, { ControlableSonicBoomEffect.packetCode } + "controlable_sonic_boom", true, { ControlableSonicBoomEffect.codec } ) val controlableGust = register( - "controlable_gust", true, { ControlableGustEffect.codec }, { ControlableGustEffect.packetCode } + "controlable_gust", true, { ControlableGustEffect.codec } ) val controlableSmallGust = register( - "controlable_small_gust", false, { ControlableSmallGustEffect.codec }, { ControlableSmallGustEffect.packetCode } + "controlable_small_gust", false, { ControlableSmallGustEffect.codec } ) val controlableFishing = register( - "controlable_fishing", false, { ControlableFishingEffect.codec }, { ControlableFishingEffect.packetCode } + "controlable_fishing", false, { ControlableFishingEffect.codec } ) val controlableFlame = register( - "controlable_flame", false, { ControlableFlameEffect.codec }, { ControlableFlameEffect.packetCode } + "controlable_flame", false, { ControlableFlameEffect.codec } ) val controlableInfested = register( - "controlable_infested", false, { ControlableInfestedEffect.codec }, { ControlableInfestedEffect.packetCode } + "controlable_infested", false, { ControlableInfestedEffect.codec } ) val controlableCherryLeaves = register( - "controlable_cherry_leaves", false, { ControlableCherryLeavesEffect.codec }, { ControlableCherryLeavesEffect.packetCode } + "controlable_cherry_leaves", false, { ControlableCherryLeavesEffect.codec } ) val controlableSculkSoul = register( - "controlable_sculk_soul", false, { ControlableSculkSoulEffect.codec }, { ControlableSculkSoulEffect.packetCode } + "controlable_sculk_soul", false, { ControlableSculkSoulEffect.codec } ) val controlableSculkChargePop = register( - "controlable_sculk_charge_pop", true, { ControlableSculkChargePopEffect.codec }, { ControlableSculkChargePopEffect.packetCode } + "controlable_sculk_charge_pop", true, { ControlableSculkChargePopEffect.codec } ) val controlableSoul = register( - "controlable_soul", false, { ControlableSoulEffect.codec }, { ControlableSoulEffect.packetCode } + "controlable_soul", false, { ControlableSoulEffect.codec } ) val controlableSoulFireFlame = register( - "controlable_soul_fire_flame", false, { ControlableSoulFireFlameEffect.codec }, { ControlableSoulFireFlameEffect.packetCode } + "controlable_soul_fire_flame", false, { ControlableSoulFireFlameEffect.codec } ) val controlableHappyVillager = register( - "controlable_happy_villager", false, { ControlableHappyVillagerEffect.codec }, { ControlableHappyVillagerEffect.packetCode } + "controlable_happy_villager", false, { ControlableHappyVillagerEffect.codec } ) val controlableHeart = register( - "controlable_heart", false, { ControlableHeartEffect.codec }, { ControlableHeartEffect.packetCode } + "controlable_heart", false, { ControlableHeartEffect.codec } ) val controlableInstantEffect = register( - "controlable_instant_effect", false, { ControlableInstantEffectParticleEffect.codec }, { ControlableInstantEffectParticleEffect.packetCode } + "controlable_instant_effect", false, { ControlableInstantEffectParticleEffect.codec } ) val controlableLargeSmoke = register( - "controlable_large_smoke", false, { ControlableLargeSmokeEffect.codec }, { ControlableLargeSmokeEffect.packetCode } + "controlable_large_smoke", false, { ControlableLargeSmokeEffect.codec } ) val controlableLava = register( - "controlable_lava", false, { ControlableLavaEffect.codec }, { ControlableLavaEffect.packetCode } + "controlable_lava", false, { ControlableLavaEffect.codec } ) val controlableMycelium = register( - "controlable_mycelium", false, { ControlableMyceliumEffect.codec }, { ControlableMyceliumEffect.packetCode } + "controlable_mycelium", false, { ControlableMyceliumEffect.codec } ) val controlableNautilus = register( - "controlable_nautilus", false, { ControlableNautilusEffect.codec }, { ControlableNautilusEffect.packetCode } + "controlable_nautilus", false, { ControlableNautilusEffect.codec } ) val controlableNote = register( - "controlable_note", false, { ControlableNoteEffect.codec }, { ControlableNoteEffect.packetCode } + "controlable_note", false, { ControlableNoteEffect.codec } ) val controlablePoof = register( - "controlable_poof", true, { ControlablePoofEffect.codec }, { ControlablePoofEffect.packetCode } + "controlable_poof", true, { ControlablePoofEffect.codec } ) val controlablePortal = register( - "controlable_portal", false, { ControlablePortalEffect.codec }, { ControlablePortalEffect.packetCode } + "controlable_portal", false, { ControlablePortalEffect.codec } ) val controlableRain = register( - "controlable_rain", false, { ControlableRainEffect.codec }, { ControlableRainEffect.packetCode } + "controlable_rain", false, { ControlableRainEffect.codec } ) val controlableSmoke = register( - "controlable_smoke", false, { ControlableSmokeEffect.codec }, { ControlableSmokeEffect.packetCode } + "controlable_smoke", false, { ControlableSmokeEffect.codec } ) val controlableWhiteSmoke = register( - "controlable_white_smoke", false, { ControlableWhiteSmokeEffect.codec }, { ControlableWhiteSmokeEffect.packetCode } + "controlable_white_smoke", false, { ControlableWhiteSmokeEffect.codec } ) val controlableSneeze = register( - "controlable_sneeze", false, { ControlableSneezeEffect.codec }, { ControlableSneezeEffect.packetCode } + "controlable_sneeze", false, { ControlableSneezeEffect.codec } ) val controlableSnowflake = register( - "controlable_snowflake", false, { ControlableSnowflakeEffect.codec }, { ControlableSnowflakeEffect.packetCode } + "controlable_snowflake", false, { ControlableSnowflakeEffect.codec } ) val controlableSpit = register( - "controlable_spit", true, { ControlableSpitEffect.codec }, { ControlableSpitEffect.packetCode } + "controlable_spit", true, { ControlableSpitEffect.codec } ) val controlableSweepAttack = register( - "controlable_sweep_attack", true, { ControlableSweepAttackEffect.codec }, { ControlableSweepAttackEffect.packetCode } + "controlable_sweep_attack", true, { ControlableSweepAttackEffect.codec } ) val controlableTotemOfUndying = register( - "controlable_totem_of_undying", false, { ControlableTotemOfUndyingEffect.codec }, { ControlableTotemOfUndyingEffect.packetCode } + "controlable_totem_of_undying", false, { ControlableTotemOfUndyingEffect.codec } ) val controlableSquidInk = register( - "controlable_squid_ink", true, { ControlableSquidInkEffect.codec }, { ControlableSquidInkEffect.packetCode } + "controlable_squid_ink", true, { ControlableSquidInkEffect.codec } ) val controlableUnderwater = register( - "controlable_underwater", false, { ControlableUnderwaterEffect.codec }, { ControlableUnderwaterEffect.packetCode } + "controlable_underwater", false, { ControlableUnderwaterEffect.codec } ) val controlableWitch = register( - "controlable_witch", false, { ControlableWitchEffect.codec }, { ControlableWitchEffect.packetCode } + "controlable_witch", false, { ControlableWitchEffect.codec } ) val controlableDrippingHoney = register( - "controlable_dripping_honey", false, { ControlableDrippingHoneyEffect.codec }, { ControlableDrippingHoneyEffect.packetCode } + "controlable_dripping_honey", false, { ControlableDrippingHoneyEffect.codec } ) val controlableFallingHoney = register( - "controlable_falling_honey", false, { ControlableFallingHoneyEffect.codec }, { ControlableFallingHoneyEffect.packetCode } + "controlable_falling_honey", false, { ControlableFallingHoneyEffect.codec } ) val controlableLandingHoney = register( - "controlable_landing_honey", false, { ControlableLandingHoneyEffect.codec }, { ControlableLandingHoneyEffect.packetCode } + "controlable_landing_honey", false, { ControlableLandingHoneyEffect.codec } ) val controlableFallingNectar = register( - "controlable_falling_nectar", false, { ControlableFallingNectarEffect.codec }, { ControlableFallingNectarEffect.packetCode } + "controlable_falling_nectar", false, { ControlableFallingNectarEffect.codec } ) val controlableFallingSporeBlossom = register( - "controlable_falling_spore_blossom", false, { ControlableFallingSporeBlossomEffect.codec }, { ControlableFallingSporeBlossomEffect.packetCode } + "controlable_falling_spore_blossom", false, { ControlableFallingSporeBlossomEffect.codec } ) val controlableSporeBlossomAir = register( - "controlable_spore_blossom_air", false, { ControlableSporeBlossomAirEffect.codec }, { ControlableSporeBlossomAirEffect.packetCode } + "controlable_spore_blossom_air", false, { ControlableSporeBlossomAirEffect.codec } ) val controlableAsh = register( - "controlable_ash", false, { ControlableAshEffect.codec }, { ControlableAshEffect.packetCode } + "controlable_ash", false, { ControlableAshEffect.codec } ) val controlableCrimsonSpore = register( - "controlable_crimson_spore", false, { ControlableCrimsonSporeEffect.codec }, { ControlableCrimsonSporeEffect.packetCode } + "controlable_crimson_spore", false, { ControlableCrimsonSporeEffect.codec } ) val controlableWarpedSpore = register( - "controlable_warped_spore", false, { ControlableWarpedSporeEffect.codec }, { ControlableWarpedSporeEffect.packetCode } + "controlable_warped_spore", false, { ControlableWarpedSporeEffect.codec } ) val controlableDrippingObsidianTear = register( - "controlable_dripping_obsidian_tear", false, { ControlableDrippingObsidianTearEffect.codec }, { ControlableDrippingObsidianTearEffect.packetCode } + "controlable_dripping_obsidian_tear", false, { ControlableDrippingObsidianTearEffect.codec } ) val controlableFallingObsidianTear = register( - "controlable_falling_obsidian_tear", false, { ControlableFallingObsidianTearEffect.codec }, { ControlableFallingObsidianTearEffect.packetCode } + "controlable_falling_obsidian_tear", false, { ControlableFallingObsidianTearEffect.codec } ) val controlableLandingObsidianTear = register( - "controlable_landing_obsidian_tear", false, { ControlableLandingObsidianTearEffect.codec }, { ControlableLandingObsidianTearEffect.packetCode } + "controlable_landing_obsidian_tear", false, { ControlableLandingObsidianTearEffect.codec } ) val controlableReversePortal = register( - "controlable_reverse_portal", false, { ControlableReversePortalEffect.codec }, { ControlableReversePortalEffect.packetCode } + "controlable_reverse_portal", false, { ControlableReversePortalEffect.codec } ) val controlableWhiteAsh = register( - "controlable_white_ash", false, { ControlableWhiteAshEffect.codec }, { ControlableWhiteAshEffect.packetCode } + "controlable_white_ash", false, { ControlableWhiteAshEffect.codec } ) val controlableSmallFlame = register( - "controlable_small_flame", false, { ControlableSmallFlameEffect.codec }, { ControlableSmallFlameEffect.packetCode } + "controlable_small_flame", false, { ControlableSmallFlameEffect.codec } ) val controlableDrippingDripstoneWater = register( - "controlable_dripping_dripstone_water", false, { ControlableDrippingDripstoneWaterEffect.codec }, { ControlableDrippingDripstoneWaterEffect.packetCode } + "controlable_dripping_dripstone_water", false, { ControlableDrippingDripstoneWaterEffect.codec } ) val controlableFallingDripstoneWater = register( - "controlable_falling_dripstone_water", false, { ControlableFallingDripstoneWaterEffect.codec }, { ControlableFallingDripstoneWaterEffect.packetCode } + "controlable_falling_dripstone_water", false, { ControlableFallingDripstoneWaterEffect.codec } ) val controlableDrippingDripstoneLava = register( - "controlable_dripping_dripstone_lava", false, { ControlableDrippingDripstoneLavaEffect.codec }, { ControlableDrippingDripstoneLavaEffect.packetCode } + "controlable_dripping_dripstone_lava", false, { ControlableDrippingDripstoneLavaEffect.codec } ) val controlableFallingDripstoneLava = register( - "controlable_falling_dripstone_lava", false, { ControlableFallingDripstoneLavaEffect.codec }, { ControlableFallingDripstoneLavaEffect.packetCode } + "controlable_falling_dripstone_lava", false, { ControlableFallingDripstoneLavaEffect.codec } ) val controlableGlowSquidInk = register( - "controlable_glow_squid_ink", true, { ControlableGlowSquidInkEffect.codec }, { ControlableGlowSquidInkEffect.packetCode } + "controlable_glow_squid_ink", true, { ControlableGlowSquidInkEffect.codec } ) val controlableGlow = register( - "controlable_glow", true, { ControlableGlowEffect.codec }, { ControlableGlowEffect.packetCode } + "controlable_glow", true, { ControlableGlowEffect.codec } ) val controlableWaxOn = register( - "controlable_wax_on", true, { ControlableWaxOnEffect.codec }, { ControlableWaxOnEffect.packetCode } + "controlable_wax_on", true, { ControlableWaxOnEffect.codec } ) val controlableWaxOff = register( - "controlable_wax_off", true, { ControlableWaxOffEffect.codec }, { ControlableWaxOffEffect.packetCode } + "controlable_wax_off", true, { ControlableWaxOffEffect.codec } ) val controlableElectricSpark = register( - "controlable_electric_spark", true, { ControlableElectricSparkEffect.codec }, { ControlableElectricSparkEffect.packetCode } + "controlable_electric_spark", true, { ControlableElectricSparkEffect.codec } ) val controlableScrape = register( - "controlable_scrape", true, { ControlableScrapeEffect.codec }, { ControlableScrapeEffect.packetCode } + "controlable_scrape", true, { ControlableScrapeEffect.codec } ) val controlableEggCrack = register( - "controlable_egg_crack", false, { ControlableEggCrackEffect.codec }, { ControlableEggCrackEffect.packetCode } + "controlable_egg_crack", false, { ControlableEggCrackEffect.codec } ) val controlableDustPlume = register( - "controlable_dust_plume", false, { ControlableDustPlumeEffect.codec }, { ControlableDustPlumeEffect.packetCode } + "controlable_dust_plume", false, { ControlableDustPlumeEffect.codec } ) val controlableTrialSpawnerDetection = register( - "controlable_trial_spawner_detection", true, { ControlableTrialSpawnerDetectionEffect.codec }, { ControlableTrialSpawnerDetectionEffect.packetCode } + "controlable_trial_spawner_detection", true, { ControlableTrialSpawnerDetectionEffect.codec } ) val controlableTrialSpawnerDetectionOminous = register( - "controlable_trial_spawner_detection_ominous", true, { ControlableTrialSpawnerDetectionOminousEffect.codec }, { ControlableTrialSpawnerDetectionOminousEffect.packetCode } + "controlable_trial_spawner_detection_ominous", true, { ControlableTrialSpawnerDetectionOminousEffect.codec } ) val controlableVaultConnection = register( - "controlable_vault_connection", true, { ControlableVaultConnectionEffect.codec }, { ControlableVaultConnectionEffect.packetCode } + "controlable_vault_connection", true, { ControlableVaultConnectionEffect.codec } ) val controlableRaidOmen = register( - "controlable_raid_omen", false, { ControlableRaidOmenEffect.codec }, { ControlableRaidOmenEffect.packetCode } + "controlable_raid_omen", false, { ControlableRaidOmenEffect.codec } ) val controlableTrialOmen = register( - "controlable_trial_omen", false, { ControlableTrialOmenEffect.codec }, { ControlableTrialOmenEffect.packetCode } + "controlable_trial_omen", false, { ControlableTrialOmenEffect.codec } ) val controlableOminousSpawning = register( - "controlable_ominous_spawning", true, { ControlableOminousSpawningEffect.codec }, { ControlableOminousSpawningEffect.packetCode } + "controlable_ominous_spawning", true, { ControlableOminousSpawningEffect.codec } ) fun reg() { @@ -313,7 +313,6 @@ object CooModParticles { fun register( id: String, alwaysShow: Boolean, codecGetter: (type: ParticleType) -> MapCodec, - packetCodec: (type: ParticleType) -> ForgeStreamCodec, ): CommonDeferredRegistry> { val registry = CommonDeferredRegistry( BuiltInRegistries.PARTICLE_TYPE, @@ -323,10 +322,6 @@ object CooModParticles { override fun codec(): MapCodec { return codecGetter(this) } - - override fun streamCodec(): StreamCodec { - return packetCodec(this) - } } } particleTypes.add(registry) 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 index 9525b328..b5cb9af4 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/SimpleControlableParticleEffect.kt @@ -6,7 +6,6 @@ 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.FriendlyByteBuf import java.util.UUID abstract class SimpleControlableParticleEffect( diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt index 8a4fb318..b6bba574 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/AutoRenderEntity.kt @@ -2,7 +2,7 @@ package cn.coostack.cooparticlesapi.renderer import cn.coostack.cooparticlesapi.annotations.codec.CodecHelper import cn.coostack.cooparticlesapi.annotations.renderer.handle.RenderEntityRegistryHelper -import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 diff --git a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt index 55db737a..69691b1e 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/RenderEntity.kt @@ -6,7 +6,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation import net.minecraft.server.level.ServerLevel import net.minecraft.world.level.Level @@ -26,7 +26,7 @@ abstract class RenderEntity(var world: Level?, var pos: Vec3 = Vec3.ZERO) : Serv private var syncOnce = false companion object { - fun decodeBase(buf: FriendlyByteBuf, instance: RenderEntity) { + fun decodeBase(buf: PacketByteBuf, instance: RenderEntity) { instance.uuid = buf.readUUID() instance.pos = buf.readVec3() instance.canceled = buf.readBoolean() @@ -34,7 +34,7 @@ abstract class RenderEntity(var world: Level?, var pos: Vec3 = Vec3.ZERO) : Serv instance.dirty = false } - fun encodeBase(buf: FriendlyByteBuf, entity: RenderEntity) { + fun encodeBase(buf: PacketByteBuf, entity: RenderEntity) { buf.writeUUID(entity.uuid) buf.writeVec3(entity.pos) buf.writeBoolean(entity.canceled) @@ -43,8 +43,8 @@ abstract class RenderEntity(var world: Level?, var pos: Vec3 = Vec3.ZERO) : Serv fun createCodec( factory: () -> T, - encodeExtra: (FriendlyByteBuf, T) -> Unit = { _, _ -> }, - decodeExtra: (FriendlyByteBuf, T) -> Unit = { _, _ -> } + encodeExtra: (PacketByteBuf, T) -> Unit = { _, _ -> }, + decodeExtra: (PacketByteBuf, T) -> Unit = { _, _ -> } ): ForgeStreamCodec { return ForgeStreamCodec.of( { buf, entity -> @@ -125,7 +125,7 @@ abstract class RenderEntity(var world: Level?, var pos: Vec3 = Vec3.ZERO) : Serv if (!force && !dirty && method == PacketRenderEntityS2C.Method.TOGGLE) { return null } - val buf = FriendlyByteBuf(Unpooled.buffer()) + val buf = PacketByteBuf(Unpooled.buffer()) getCodec().encode(buf, this) val bytes = ByteArray(buf.readableBytes()) buf.readBytes(bytes) 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 index f37e5507..7fc37ca6 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectParams.kt @@ -1,63 +1,63 @@ package cn.coostack.cooparticlesapi.renderer.post import cn.coostack.cooparticlesapi.renderer.pipeline.CooUniformValue -import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation internal sealed interface PostEffectParamValue { - fun write(buf: FriendlyByteBuf) + fun write(buf: PacketByteBuf) data class BoolValue(val value: Boolean) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeBoolean(value) } } data class IntValue(val value: Int) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeInt(value) } } data class LongValue(val value: Long) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeLong(value) } } data class FloatValue(val value: Float) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeFloat(value) } } data class DoubleValue(val value: Double) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeDouble(value) } } data class StringValue(val value: String) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeUtf(value) } } data class ResourceValue(val value: ResourceLocation) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeResourceLocation(value) } } data class Vec2Value(val x: Float, val y: Float) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + 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: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeDouble(x) buf.writeDouble(y) buf.writeDouble(z) @@ -65,7 +65,7 @@ internal sealed interface PostEffectParamValue { } data class ColorValue(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { buf.writeFloat(red) buf.writeFloat(green) buf.writeFloat(blue) @@ -74,18 +74,18 @@ internal sealed interface PostEffectParamValue { } data class UniformValue(val value: CooUniformValue) : PostEffectParamValue { - override fun write(buf: FriendlyByteBuf) { + override fun write(buf: PacketByteBuf) { CooUniformValue.STREAM_CODEC.encode(buf, value) } } companion object { - fun writeTyped(buf: FriendlyByteBuf, value: PostEffectParamValue) { + fun writeTyped(buf: PacketByteBuf, value: PostEffectParamValue) { buf.writeUtf(value.typeId) value.write(buf) } - fun readTyped(buf: FriendlyByteBuf): PostEffectParamValue { + fun readTyped(buf: PacketByteBuf): PostEffectParamValue { return when (val type = buf.readUtf()) { "bool" -> BoolValue(buf.readBoolean()) "int" -> IntValue(buf.readInt()) @@ -150,7 +150,7 @@ internal data class PostEffectParams( fun plus(name: String, value: PostEffectParamValue): PostEffectParams = PostEffectParams(values + (name to value)) - fun write(buf: FriendlyByteBuf) { + fun write(buf: PacketByteBuf) { buf.writeInt(values.size) values.toSortedMap().forEach { (name, value) -> buf.writeUtf(name) @@ -161,7 +161,7 @@ internal data class PostEffectParams( companion object { val EMPTY = PostEffectParams() - fun read(buf: FriendlyByteBuf): PostEffectParams { + fun read(buf: PacketByteBuf): PostEffectParams { val count = buf.readInt() val values = LinkedHashMap(count) repeat(count) { 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 index ce833e94..e4db5235 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityRegistry.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityRegistry.kt @@ -1,7 +1,7 @@ package cn.coostack.cooparticlesapi.renderer.runtime import cn.coostack.cooparticlesapi.renderer.RenderEntity -import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation object ClientRenderEntityRegistry { 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 index 4a2692cc..9c627376 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityType.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/ClientRenderEntityType.kt @@ -1,7 +1,7 @@ package cn.coostack.cooparticlesapi.renderer.runtime import cn.coostack.cooparticlesapi.renderer.RenderEntity -import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf data class ClientRenderEntityType( val codec: ForgeStreamCodec, 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 index eef40634..12ed400b 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/RenderEntityAutoRegistry.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/runtime/RenderEntityAutoRegistry.kt @@ -7,7 +7,7 @@ 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.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.resources.ResourceLocation import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 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 index 094e58c8..45d134b8 100644 --- a/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/terrain/CooTerrainMappingRegion.kt +++ b/forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/terrain/CooTerrainMappingRegion.kt @@ -1,6 +1,6 @@ package cn.coostack.cooparticlesapi.renderer.terrain -import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.PacketByteBuf import net.minecraft.world.phys.Vec3 import kotlin.math.abs import kotlin.math.ceil @@ -11,7 +11,7 @@ sealed interface CooTerrainMappingRegion { fun contains(position: Vec3): Boolean - fun encode(buffer: FriendlyByteBuf) { + fun encode(buffer: PacketByteBuf) { buffer.writeVarInt(WIRE_VERSION) buffer.writeResourceLocation(type.id) when (this) { @@ -186,7 +186,7 @@ sealed interface CooTerrainMappingRegion { companion object { private const val WIRE_VERSION = 2 - fun decode(buffer: FriendlyByteBuf): CooTerrainMappingRegion { + 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() 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 index 16c865e9..b74ee7d6 100644 --- 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 @@ -1,13 +1,13 @@ package cn.coostack.cooparticlesapi.utils.interpolator.data import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.network.FriendlyByteBuf + class InterpolatorDouble(value: Double) : AbstractInterpolatorData(value) { companion object { @JvmStatic - val CODEC = ForgeStreamCodec.of( + val CODEC = ForgeStreamCodec.of(InterpolatorDouble>( { buf, data -> buf.writeDouble(data.value) }, { 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 index ff08d6b4..d6d5e89b 100644 --- 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 @@ -1,13 +1,13 @@ package cn.coostack.cooparticlesapi.utils.interpolator.data import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.network.FriendlyByteBuf + class InterpolatorFloat(value: Float) : AbstractInterpolatorData(value) { companion object { @JvmStatic - val CODEC = ForgeStreamCodec.of( + val CODEC = ForgeStreamCodec.of(InterpolatorFloat>( { buf, data -> buf.writeFloat(data.value) }, { 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 index 704d4cdf..ac051e46 100644 --- 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 @@ -1,14 +1,14 @@ package cn.coostack.cooparticlesapi.utils.interpolator.data import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.network.FriendlyByteBuf + import org.joml.Quaternionf class InterpolatorQuaternionf(value: Quaternionf) : AbstractInterpolatorData(value) { companion object { @JvmStatic - val CODEC = ForgeStreamCodec.of( + val CODEC = ForgeStreamCodec.of(InterpolatorQuaternionf>( { buf, data -> buf.writeFloat(data.value.x) buf.writeFloat(data.value.y) 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 index 388a8768..546b3bab 100644 --- 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 @@ -2,14 +2,14 @@ package cn.coostack.cooparticlesapi.utils.interpolator.data import cn.coostack.cooparticlesapi.utils.GraphMathHelper import cn.coostack.cooparticlesapi.utils.RelativeLocation -import net.minecraft.network.FriendlyByteBuf + import net.minecraft.world.phys.Vec3 class InterpolatorRelativeLocation(value: RelativeLocation) : AbstractInterpolatorData(value) { companion object { @JvmStatic - val CODEC = ForgeStreamCodec.of( + val CODEC = ForgeStreamCodec.of(InterpolatorRelativeLocation>( { buf, data -> buf.writeDouble(data.value.x) buf.writeDouble(data.value.y) 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 index 17327b5d..b06c7da3 100644 --- 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 @@ -1,14 +1,14 @@ package cn.coostack.cooparticlesapi.utils.interpolator.data import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.network.FriendlyByteBuf + import net.minecraft.world.phys.Vec3 class InterpolatorVec3d(value: Vec3) : AbstractInterpolatorData(value) { companion object { @JvmStatic - val CODEC = ForgeStreamCodec.of( + val CODEC = ForgeStreamCodec.of(InterpolatorVec3d>( { buf, data -> buf.writeVec3(data.value) }, { 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 index 1d308c41..e5de0ded 100644 --- 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 @@ -1,14 +1,14 @@ package cn.coostack.cooparticlesapi.utils.interpolator.data import cn.coostack.cooparticlesapi.utils.GraphMathHelper -import net.minecraft.network.FriendlyByteBuf + import org.joml.Vector3f class InterpolatorVector3f(value: Vector3f) : AbstractInterpolatorData(value) { companion object { @JvmStatic - val CODEC = ForgeStreamCodec.of( + val CODEC = ForgeStreamCodec.of(InterpolatorVector3f>( { buf, data -> buf.writeFloat(data.value.x()) buf.writeFloat(data.value.y()) From 4a5a84df6f222ea3e8cf3fdc6a9e9aac9c76abe8 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 21:34:57 +0800 Subject: [PATCH 19/41] fix: complete Forge MC 1.20.1 API compatibility pass - Replace all remaining FriendlyByteBuf/StreamCodec/CustomPacketPayload/ByteBufCodecs/RegistryFriendlyByteBuf usages in forge/ - Create Forge overrides for: CodecHelper, DisplayEntityRegistryHelper, RenderEntityRegistryHelper, Controlable*Effect classes, PostEffectBinding, PostEffectLifecycle - Update common CodecHelper and DataHolderManager to use CommonStreamCodec instead of StreamCodec - Remove streamCodec() override from Forge CooModParticles particle registration - Fix Forge networking implementations (ForgeServerNetworking, ForgeClientNetworking, ForgeServerContext) - Add Forge particle codec helper (ForgeCodecHelper.particleCodecOf) using writeToPacket/readFromPacket - Trigger CI to verify :forge:compileKotlin --- .../annotations/codec/CodecHelper.kt | 30 +- .../annotations/codec/CodecHelper.kt | 678 ++++++++++++++++++ .../handle/DisplayEntityRegistryHelper.kt | 47 ++ .../handle/RenderEntityRegistryHelper.kt | 71 ++ .../particles/impl/ControlableCloudEffect.kt | 61 ++ .../impl/ControlableEnchantmentEffect.kt | 59 ++ .../particles/impl/ControlableEndRodEffect.kt | 61 ++ .../impl/ControlableFallingDustEffect.kt | 98 +++ .../impl/ControlableFireworkEffect.kt | 61 ++ .../particles/impl/ControlableFlashEffect.kt | 61 ++ .../particles/impl/ControlableSplashEffect.kt | 61 ++ .../renderer/post/PostEffectBinding.kt | 291 ++++++++ .../renderer/post/PostEffectLifecycle.kt | 113 +++ gradlew | 0 14 files changed, 1677 insertions(+), 15 deletions(-) create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/codec/CodecHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/display/handle/DisplayEntityRegistryHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/annotations/renderer/handle/RenderEntityRegistryHelper.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableCloudEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEnchantmentEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableEndRodEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFallingDustEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFireworkEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableFlashEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/particles/impl/ControlableSplashEffect.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectBinding.kt create mode 100644 forge/src/main/kotlin/cn/coostack/cooparticlesapi/renderer/post/PostEffectLifecycle.kt mode change 100644 => 100755 gradlew 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 1abcec80..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 @@ -174,7 +174,7 @@ object CodecHelper { ) register( BlockPos::class.java, - CommonStreamCodec.of(BlockPos.STREAM_CODEC::encode, BlockPos.STREAM_CODEC::decode) + CommonStreamCodec.of({ buf, pos -> buf.writeBlockPos(pos) }, { buf -> buf.readBlockPos() }) ) register( BlockState::class.java, @@ -496,9 +496,9 @@ object CodecHelper { } 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 -> @@ -523,9 +523,9 @@ object CodecHelper { } 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 -> @@ -551,10 +551,10 @@ object CodecHelper { 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,8 +585,8 @@ object CodecHelper { @Suppress("UNCHECKED_CAST") 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,8 +612,8 @@ object CodecHelper { @Suppress("UNCHECKED_CAST") 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,10 +641,10 @@ object CodecHelper { @Suppress("UNCHECKED_CAST") 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/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/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/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/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/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/gradlew b/gradlew old mode 100644 new mode 100755 From efdf2709a13926d51791e8fec7162404ba09f070 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 21:54:32 +0800 Subject: [PATCH 20/41] fix: configure Forge Gradle plugin resolution for CI - Add net.minecraftforge.gradle plugin to root build.gradle (version 6.0.24) - Add buildscript block to Forge module build.gradle with Forge maven repo - Update gradle.properties: forge_version=1.20.1-46.0.14, loader range [46,) - Fixes CI failure: 'Plugin net.minecraftforge.gradle was not found' --- forge/build.gradle | 16 +++++++++++++++- gradle.properties | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index a31ab462..5dfe7321 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,6 +1,20 @@ +buildscript { + repositories { + maven { + name = "Forge" + url = uri("https://maven.minecraftforge.net") + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath "net.minecraftforge.gradle:ForgeGradle:6.0.24" + } +} + plugins { id 'multiloader-loader' - id 'net.minecraftforge.gradle' + id 'net.minecraftforge.gradle' version '6.0.24' } tasks.javadoc { diff --git a/gradle.properties b/gradle.properties index aa5b5593..a9a7b55c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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,) From 06fd62bc35c1854cb06dc56bde75b6e7ec0e42c6 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 22:04:08 +0800 Subject: [PATCH 21/41] fix: resolve Forge Gradle plugin in settings.gradle pluginManagement - Remove buildscript block from Forge build.gradle (not allowed with exclusiveContent) - Add net.minecraftforge.gradle 6.0.24 to settings.gradle pluginManagement - Fixes Gradle 8.x exclusive repository content restriction --- settings.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.gradle b/settings.gradle index 589f3402..02c3bc32 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,7 @@ pluginManagement { } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' + id "net.minecraftforge.gradle" version "6.0.24" } } From c740751dc79c3ed269c80959e5f03a41f10a5090 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 22:19:46 +0800 Subject: [PATCH 22/41] fix: resolve Forge Gradle plugin and task configuration issues - Remove exclusiveContent from Forge repository in settings.gradle - Remove buildscript block from forge/build.gradle - Fix sourceWithCommon task to not depend on non-existent sourcesJar in Forge module - Add net.minecraftforge.gradle plugin to root build.gradle and settings.gradle --- forge/build.gradle | 17 ----------------- settings.gradle | 13 +++---------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index 5dfe7321..fb1a3d4c 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,17 +1,3 @@ -buildscript { - repositories { - maven { - name = "Forge" - url = uri("https://maven.minecraftforge.net") - } - gradlePluginPortal() - mavenCentral() - } - dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:6.0.24" - } -} - plugins { id 'multiloader-loader' id 'net.minecraftforge.gradle' version '6.0.24' @@ -47,9 +33,6 @@ minecraft { } tasks.register("sourceWithCommon", Jar) { - def current = tasks.named("sourcesJar").get() - dependsOn(current) - from current.outputs.files.collect { zipTree(it) } def other = project(":common").tasks.named("sourcesJar") dependsOn(other) from zipTree(other.get().archiveFile) diff --git a/settings.gradle b/settings.gradle index 02c3bc32..8f6a3dbe 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,16 +25,9 @@ pluginManagement { includeGroupAndSubgroups("org.spongepowered") } } - 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 { From 0e9589b4e68a345de8d4899fffd4faa6b9c2a243 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Sun, 20 Sep 2026 22:33:17 +0800 Subject: [PATCH 23/41] fix: use correct ForgeGradle version and Minecraft version for 1.20.1 - Downgrade ForgeGradle from 6.0.24 to 5.2.5 (compatible with 1.20.1) - Set minecraft.version to '1.20.1' in Forge build.gradle - Remove exclusiveContent from Forge repo in settings.gradle --- forge/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index fb1a3d4c..6d905cdb 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,6 +1,6 @@ plugins { id 'multiloader-loader' - id 'net.minecraftforge.gradle' version '6.0.24' + id 'net.minecraftforge.gradle' version '5.2.5' } tasks.javadoc { @@ -12,7 +12,7 @@ tasks.named('processResources', ProcessResources) { } minecraft { - version = forge_version + version = '1.20.1' mappings channel: 'parchment', version: "${parchment_minecraft}-${parchment_version}" runs { client { From 0455f62f031ec788a17f70cfd4e0fe655fcef635 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:02:19 +0800 Subject: [PATCH 24/41] fix: configure ForgeGradle 5.2.5 via buildscript block - Add buildscript block to forge/build.gradle with Forge maven repo - Remove net.minecraftforge.gradle from settings.gradle pluginManagement - ForgeGradle 5.2.5 uses legacy buildscript resolution, not pluginManagement --- forge/build.gradle | 14 ++++++++++++++ settings.gradle | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index 6d905cdb..e2054fdb 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,3 +1,17 @@ +buildscript { + repositories { + maven { + name = "Forge" + url = uri("https://maven.minecraftforge.net") + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath "net.minecraftforge.gradle:ForgeGradle:5.2.5" + } +} + plugins { id 'multiloader-loader' id 'net.minecraftforge.gradle' version '5.2.5' diff --git a/settings.gradle b/settings.gradle index 8f6a3dbe..286b4a8f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,7 +32,6 @@ pluginManagement { } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' - id "net.minecraftforge.gradle" version "6.0.24" } } From 79c5b61a53bcb9b9e4d178fa376edee86effbd92 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:26:30 +0800 Subject: [PATCH 25/41] fix: resolve Gradle exclusiveContent restriction for Forge plugin - Remove all exclusiveContent blocks from settings.gradle pluginManagement - Add net.minecraftforge.gradle 5.2.5 to settings.gradle plugins - Remove buildscript block from forge/build.gradle - Fixes: 'When using exclusive repository content, you cannot add repositories to buildscript.repositories' --- settings.gradle | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/settings.gradle b/settings.gradle index 286b4a8f..6a22b8fb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,28 +2,13 @@ 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') } maven { name = 'Forge' @@ -32,6 +17,7 @@ pluginManagement { } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' + id "net.minecraftforge.gradle" version "5.2.5" } } From 3afb4e1f3751605a4493ece4572f3dfc05c3059b Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:32:38 +0800 Subject: [PATCH 26/41] fix: upgrade ForgeGradle to 6.0.24 for Gradle 8.x compatibility - ForgeGradle 6.0.24 supports both 1.20.1 and Gradle 8.x - Update version in forge/build.gradle and settings.gradle --- forge/build.gradle | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index e2054fdb..7e3218c3 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -14,7 +14,7 @@ buildscript { plugins { id 'multiloader-loader' - id 'net.minecraftforge.gradle' version '5.2.5' + id 'net.minecraftforge.gradle' version '6.0.24' } tasks.javadoc { diff --git a/settings.gradle b/settings.gradle index 6a22b8fb..c89941af 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,7 +17,7 @@ pluginManagement { } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' - id "net.minecraftforge.gradle" version "5.2.5" + id "net.minecraftforge.gradle" version "6.0.24" } } From fec7099909b61be3b3a516e19b44c25968865295 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:37:14 +0800 Subject: [PATCH 27/41] fix: update Forge build.gradle for 1.20.1 compatibility - Remove buildscript block (not needed with plugins DSL) - Use parchment 1.20.1-2024.07.07 instead of 1.21.1 mappings - Fixes 'Cannot remap' errors and '> version' evaluation failure --- forge/build.gradle | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index 7e3218c3..4da08cfc 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,17 +1,3 @@ -buildscript { - repositories { - maven { - name = "Forge" - url = uri("https://maven.minecraftforge.net") - } - gradlePluginPortal() - mavenCentral() - } - dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:5.2.5" - } -} - plugins { id 'multiloader-loader' id 'net.minecraftforge.gradle' version '6.0.24' @@ -27,7 +13,7 @@ tasks.named('processResources', ProcessResources) { minecraft { version = '1.20.1' - mappings channel: 'parchment', version: "${parchment_minecraft}-${parchment_version}" + mappings channel: 'parchment', version: '1.20.1-2024.07.07' runs { client { workingDirectory project.file('run') From 44e08a6e4fd35073de1017c9774d5b1590a78d14 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:40:55 +0800 Subject: [PATCH 28/41] fix: try minecraft_version property in Forge build.gradle --- forge/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index 4da08cfc..8b5dcb0c 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -12,7 +12,7 @@ tasks.named('processResources', ProcessResources) { } minecraft { - version = '1.20.1' + minecraft_version = '1.20.1' mappings channel: 'parchment', version: '1.20.1-2024.07.07' runs { client { From b5cd9e6d102c55cefd227fb6c2335829e27ea338 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:45:24 +0800 Subject: [PATCH 29/41] fix: revert to ForgeGradle 5.2.5 for 1.20.1 support - ForgeGradle 6.0.x does not support Minecraft 1.20.1 - Restore buildscript block in forge/build.gradle - Update settings.gradle pluginManagement --- forge/build.gradle | 18 ++++++++++++++++-- settings.gradle | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index 8b5dcb0c..6a611950 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,6 +1,20 @@ +buildscript { + repositories { + maven { + name = "Forge" + url = uri("https://maven.minecraftforge.net") + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath "net.minecraftforge.gradle:ForgeGradle:5.2.5" + } +} + plugins { id 'multiloader-loader' - id 'net.minecraftforge.gradle' version '6.0.24' + id 'net.minecraftforge.gradle' version '5.2.5' } tasks.javadoc { @@ -12,7 +26,7 @@ tasks.named('processResources', ProcessResources) { } minecraft { - minecraft_version = '1.20.1' + version = '1.20.1' mappings channel: 'parchment', version: '1.20.1-2024.07.07' runs { client { diff --git a/settings.gradle b/settings.gradle index c89941af..6a22b8fb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,7 +17,7 @@ pluginManagement { } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' - id "net.minecraftforge.gradle" version "6.0.24" + id "net.minecraftforge.gradle" version "5.2.5" } } From 6e05303d12afb6f275e72414bab134acc773086f Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:49:45 +0800 Subject: [PATCH 30/41] fix: use buildscript + apply plugin for ForgeGradle 5.2.5 - ForgeGradle 5.2.5 is not published as a Gradle plugin for Gradle 8.x - Use buildscript block + apply plugin instead of plugins DSL - Remove Forge plugin from settings.gradle pluginManagement --- forge/build.gradle | 3 ++- settings.gradle | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index 6a611950..7b32d45b 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -14,9 +14,10 @@ buildscript { plugins { id 'multiloader-loader' - id 'net.minecraftforge.gradle' version '5.2.5' } +apply plugin: 'net.minecraftforge.gradle' + tasks.javadoc { enabled = false } diff --git a/settings.gradle b/settings.gradle index 6a22b8fb..893615e2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,8 +17,7 @@ pluginManagement { } plugins { id "org.jetbrains.kotlin.kapt" version '2.2.0' - id "net.minecraftforge.gradle" version "5.2.5" - } + } } plugins { From cc68dfe7188a77a8c23da347c10a7ba907cac04f Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 08:54:06 +0800 Subject: [PATCH 31/41] fix: try ForgeGradle 5.1.0 for 1.20.1 --- forge/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index 7b32d45b..8f8920d8 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:5.2.5" + classpath "net.minecraftforge.gradle:ForgeGradle:5.1.0" } } From 87a6659618daebb54900bae645d3a022b16ef248 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:00:11 +0800 Subject: [PATCH 32/41] fix: try method call syntax for minecraft version --- forge/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index 8f8920d8..2efe6aba 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -27,7 +27,7 @@ tasks.named('processResources', ProcessResources) { } minecraft { - version = '1.20.1' + version '1.20.1' mappings channel: 'parchment', version: '1.20.1-2024.07.07' runs { client { From b8ca033c95a645fc2d8820b92521f68134ffb5cb Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:04:35 +0800 Subject: [PATCH 33/41] fix: replace additionalRuntimeClasspath with runtimeOnly --- forge/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index 2efe6aba..dc412dc8 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -87,7 +87,7 @@ dependencies { 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" - additionalRuntimeClasspath fileTree(dir: "../depend", includes: ["*.jar"]) + runtimeOnly fileTree(dir: "../depend", includes: ["*.jar"]) } sourceSets.main.resources { From 94f7d33c912f7c9e392f00913cb692be0b586f4e Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:08:55 +0800 Subject: [PATCH 34/41] fix: add explicit minecraft and forge dependencies --- forge/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge/build.gradle b/forge/build.gradle index dc412dc8..dcc96db9 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -84,9 +84,11 @@ repositories { } 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"]) } From 47fb8cb426eb77e7b954c8d3ca1d995b5034ee50 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:13:21 +0800 Subject: [PATCH 35/41] fix: try ForgeGradle 5.1.44 for 1.20.1 --- forge/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index dcc96db9..6d5feb68 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:5.1.0" + classpath "net.minecraftforge.gradle:ForgeGradle:5.1.44" } } From e2a3fc84d45194e540b5e5c4fd84080e9e3cd2ac Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:18:13 +0800 Subject: [PATCH 36/41] fix: downgrade Gradle to 8.1 for ForgeGradle 5.1.x compatibility --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 106cc44dfef17fc60858587a3f82cf18aea88466 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:23:58 +0800 Subject: [PATCH 37/41] fix: try ForgeGradle 6.0.24 with buildscript approach --- forge/build.gradle | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index 6d5feb68..d4d452b2 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'multiloader-loader' +} + buildscript { repositories { maven { @@ -8,14 +12,10 @@ buildscript { mavenCentral() } dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:5.1.44" + classpath "net.minecraftforge.gradle:ForgeGradle:6.0.24" } } -plugins { - id 'multiloader-loader' -} - apply plugin: 'net.minecraftforge.gradle' tasks.javadoc { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0e1ae8ce..e18bc253 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.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 84ad0697497fc3a97eff20188b0d1eda846310b5 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:35:27 +0800 Subject: [PATCH 38/41] fix: reorder buildscript block and fix sourceWithCommon task --- forge/build.gradle | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index d4d452b2..70f22740 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,8 +1,20 @@ +buildscript { + repositories { + maven { + name = "Forge" + url = uri("https://maven.minecraftforge.net") + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath "net.minecraftforge.gradle:ForgeGradle:6.0.24" + } +} + plugins { id 'multiloader-loader' } - -buildscript { repositories { maven { name = "Forge" @@ -48,9 +60,11 @@ minecraft { } tasks.register("sourceWithCommon", Jar) { - def other = project(":common").tasks.named("sourcesJar") - dependsOn(other) - from zipTree(other.get().archiveFile) + 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" From 4d2a17342cd11662fdbf547b94ae0fbe81e5ae71 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:41:20 +0800 Subject: [PATCH 39/41] fix: handle missing sourcesJar task in root build.gradle --- build.gradle | 10 ++++++---- forge/build.gradle | 12 ------------ 2 files changed, 6 insertions(+), 16 deletions(-) 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/forge/build.gradle b/forge/build.gradle index 70f22740..29896663 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -15,18 +15,6 @@ buildscript { plugins { id 'multiloader-loader' } - repositories { - maven { - name = "Forge" - url = uri("https://maven.minecraftforge.net") - } - gradlePluginPortal() - mavenCentral() - } - dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:6.0.24" - } -} apply plugin: 'net.minecraftforge.gradle' From e83c718b5bf42d7aeb536ccb196c20c69680f441 Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 09:47:07 +0800 Subject: [PATCH 40/41] fix: try ForgeGradle 5.1.50 --- forge/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/build.gradle b/forge/build.gradle index 29896663..c943e675 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath "net.minecraftforge.gradle:ForgeGradle:6.0.24" + classpath "net.minecraftforge.gradle:ForgeGradle:5.1.50" } } From 9f16736e9e3703eea778c1844ee773eecc1a940e Mon Sep 17 00:00:00 2001 From: Meeuyt Date: Mon, 21 Sep 2026 10:09:11 +0800 Subject: [PATCH 41/41] =?UTF-8?q?fix:=20=E6=95=B4=E4=BD=93=E9=99=8D?= =?UTF-8?q?=E7=BA=A7=E5=88=B0=20Java=2017=20+=20Gradle=208.1=20=E4=BB=A5?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20ForgeGradle=205.1.x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gradle.properties: java_version=21 -> 17 - gradle-wrapper: 8.12.1 -> 8.1 - .github/workflows/gradle.yml: setup-java 21 -> 17 - ForgeGradle 5.1.x 需要 Gradle 8.1 及 Java 17 --- .github/workflows/gradle.yml | 2 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 735fe6c6..de33e7c4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -28,7 +28,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: "21" + java-version: "17" cache: gradle # 可选:更安全,防止 wrapper 被篡改(不想要可删) diff --git a/gradle.properties b/gradle.properties index a9a7b55c..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 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