31 Commits
Author SHA1 Message Date
ByteDice 2423efbd92 ready for 1.0! Idc that it may be buggy, I want 1.0 now! 2024-12-29 15:21:06 +01:00
ByteDice 46498d25f4 added emitter debug tools. More testing required before 1.0! 2024-12-26 16:34:19 +01:00
ByteDice c416432f7b updated particle debug tools (emitter debug coming soon) 2024-12-25 16:29:32 +01:00
ByteDice a7212dbe42 removed TODO 2024-12-23 22:46:30 +01:00
ByteDice fdea77092b added new parameters 2024-12-23 16:03:13 +01:00
ByteDice 22027610ea updated the BDE to handle extra properties 2024-12-23 15:13:41 +01:00
ByteDiceandGitHub 4c051ec292 Update Bde_particles.kt TODOs 2024-12-22 23:24:53 +01:00
ByteDiceandGitHub adfa1b7bf2 Update README.md 2024-12-22 22:29:31 +01:00
ByteDice 45ff607f6a fixed some bugs. Testing time! 2024-12-22 17:49:04 +01:00
ByteDice 3ff032db1d if it works then im not touching it anymore 2024-12-22 17:06:16 +01:00
ByteDice 3b47ad870f fixed bug when compiling JAR file 2024-12-15 18:53:52 +01:00
ByteDice ddd78dcef4 fixed bug where creating a particle with a minecraft: prefix crashes server 2024-12-15 15:29:39 +01:00
ByteDice b1832825a8 fixed all bugs on ConfigArgs.kt 2024-12-15 14:31:59 +01:00
ByteDice 3e08bd4d81 tested & fixed some bugs for ConfigArgs.kt 2024-12-15 10:57:59 +01:00
ByteDice 7716ba30f8 added force field arg. Testing required for ALL args 2024-12-14 19:17:42 +01:00
ByteDice 5c1b1b07ce added more args 2024-12-14 13:11:23 +01:00
ByteDice 46cc808889 added args 2024-12-13 20:21:43 +01:00
ByteDice 0b17f8688c remade ManageEmitters command (except config). Added SpawnEmitter command. 2024-12-07 14:31:34 +01:00
ByteDice ad9ef87874 uhhhhh 2024-12-05 12:14:06 +01:00
ByteDice e2049fd204 i forgor 2024-12-02 20:27:07 +01:00
ByteDice 6eea92033d i forgor 2024-12-02 19:11:12 +01:00
ByteDice 13113e4c18 updated TODOs, canceled cone & cylinder SDF :( 2024-12-01 15:24:00 +01:00
ByteDice c5cc910be3 added async particle ticking and made new params fully working. 2024-12-01 15:13:41 +01:00
ByteDice 2edb23e9ec bugfixes 2024-11-30 19:52:25 +01:00
ByteDice 828297aef2 moved to ItemDisplays and added support for CustomModelData 2024-11-30 17:22:20 +01:00
ByteDice 8ad6cb9ffb made some newly added params function correctly 2024-11-30 13:08:35 +01:00
ByteDice be00598efe added some new params to Particle 2024-11-30 00:05:31 +01:00
ByteDice 939867934b significantly changed emitter params. STILL A LOT OF ERRORS. 2024-11-29 19:38:43 +01:00
ByteDice 6728445280 made particles compatible with updated param data. Though not the new ones. STILL FILLED WITH ERRORS 2024-11-28 20:29:37 +01:00
ByteDice 5b43c12c15 reworked a bunch of stuff. Contains errors 2024-11-28 19:53:49 +01:00
ByteDiceandGitHub af7d29ce7d Update issue templates 2024-11-25 20:08:26 +01:00
24 changed files with 2535 additions and 1404 deletions
+5 -13
View File
@@ -1,8 +1,8 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us squash pests
title: '' title: ''
labels: '' labels: bug
assignees: '' assignees: ''
--- ---
@@ -10,6 +10,9 @@ assignees: ''
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**Minecraft version**
Provide your Minecraft version, even if the mod is only for a single version.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
@@ -23,16 +26,5 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
+4 -4
View File
@@ -2,19 +2,19 @@
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: ''
labels: '' labels: enhancement
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem?**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like** **Describe the result you'd like**
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
**Describe alternatives you've considered** **Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative features you've considered.
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.
+9 -2
View File
@@ -3,8 +3,15 @@
BDE_ParticleSys is a highly customizable **server-side** particle system that uses `Block Display Entities` (BDEs) as particles. It is meant to be used as a library for other mods, but can be used as a standalone mod too. BDE_ParticleSys is a highly customizable **server-side** particle system that uses `Block Display Entities` (BDEs) as particles. It is meant to be used as a library for other mods, but can be used as a standalone mod too.
<!-- change GiveParticleEmitter to GiveEmitterTool --> If BDE_ParticleSys is used as a standalone mod, you will only be able to use it with the custom in-game commands `/GiveEmitterTool`, `/KillAllEmitters`, and `/ManageEmitters`. Any particles made, removed, or changed with those commands are **temporary** and get reverted once the server shuts down, as they are meant for quick testing.
If BDE_ParticleSys is used as a standalone mod, you will only be able to use it with the custom in-game commands `/GiveEmitterTool`, `/KillAllEmitters` and `/ManageEmitters`. Any particles made with those commands are **temporary** and get removed once the server shuts down, as they are meant for quick testing.
### In-game commands (does not save):
* `/GiveEmitterTool`
* `/SpawnEmitter`
* `/KillAllEmitters`
* `/ManageEmitters`
* `GlobalMaxParticles` (GameRule)
* `ShowParticleDebug` (GameRule)
# Open-source - Copyright # Open-source - Copyright
@@ -3,6 +3,8 @@ package com.bytedice.bde_particles.client
import com.bytedice.bde_particles.commands.GiveEmitterTool import com.bytedice.bde_particles.commands.GiveEmitterTool
import com.bytedice.bde_particles.commands.KillAllEmitters import com.bytedice.bde_particles.commands.KillAllEmitters
import com.bytedice.bde_particles.commands.ManageEmitters import com.bytedice.bde_particles.commands.ManageEmitters
import com.bytedice.bde_particles.commands.SpawnEmitter
//import com.bytedice.bde_particles.commands.ManageEmitters
import com.bytedice.bde_particles.init import com.bytedice.bde_particles.init
import com.bytedice.bde_particles.onRightClick import com.bytedice.bde_particles.onRightClick
import com.bytedice.bde_particles.tick import com.bytedice.bde_particles.tick
@@ -38,8 +40,9 @@ class Bde_particlesClient : ClientModInitializer {
CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, _ -> CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, _ ->
GiveEmitterTool.register(dispatcher, registryAccess) GiveEmitterTool.register(dispatcher, registryAccess)
ManageEmitters .register(dispatcher) ManageEmitters .register(dispatcher, registryAccess)
KillAllEmitters.register(dispatcher) KillAllEmitters.register(dispatcher)
SpawnEmitter .register(dispatcher)
} }
UseItemCallback.EVENT.register(UseItemCallback { player: PlayerEntity, world: World, hand: Hand -> UseItemCallback.EVENT.register(UseItemCallback { player: PlayerEntity, world: World, hand: Hand ->
@@ -3,35 +3,80 @@ package com.bytedice.bde_particles
import com.bytedice.bde_particles.commands.GiveEmitterTool import com.bytedice.bde_particles.commands.GiveEmitterTool
import com.bytedice.bde_particles.commands.KillAllEmitters import com.bytedice.bde_particles.commands.KillAllEmitters
import com.bytedice.bde_particles.commands.ManageEmitters import com.bytedice.bde_particles.commands.ManageEmitters
import com.bytedice.bde_particles.commands.SpawnEmitter
import com.bytedice.bde_particles.items.ParticleEmitterTool import com.bytedice.bde_particles.items.ParticleEmitterTool
import com.bytedice.bde_particles.particles.* import com.bytedice.bde_particles.particles.*
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import net.fabricmc.api.EnvType import net.fabricmc.api.EnvType
import net.fabricmc.api.ModInitializer import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents
import net.fabricmc.fabric.api.event.player.UseItemCallback import net.fabricmc.fabric.api.event.player.UseItemCallback
import net.fabricmc.fabric.api.gamerule.v1.GameRuleFactory
import net.fabricmc.fabric.api.gamerule.v1.GameRuleRegistry
import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.FabricLoader
import net.minecraft.entity.EntityType import net.minecraft.entity.EntityType
import net.minecraft.entity.decoration.DisplayEntity.BlockDisplayEntity import net.minecraft.entity.decoration.DisplayEntity.ItemDisplayEntity
import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.server.MinecraftServer import net.minecraft.server.MinecraftServer
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.util.ActionResult import net.minecraft.util.ActionResult
import net.minecraft.util.Hand import net.minecraft.util.Hand
import net.minecraft.util.TypedActionResult import net.minecraft.util.TypedActionResult
import net.minecraft.world.GameRules
import net.minecraft.world.World import net.minecraft.world.World
import org.joml.Vector2f
import java.awt.Color
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.declaredMemberProperties
// TODO: (future additions)
// Cylinder and cone force field shape.
// Copying particle params in-game.
// Custom curve equation command args (parse from strings).
// Force field "config" option.
// add rotation offset to transformWithVel
// TODO: (now)
// nothing lmao
var ALL_PARTICLE_EMITTERS: Array<ParticleEmitter> = emptyArray() var ALL_PARTICLE_EMITTERS: Array<ParticleEmitter> = emptyArray()
val sessionUuid: UUID = UUID.randomUUID() var LIVING_PARTICLE_COUNT: Int = 0
val SESSION_UUID: UUID = UUID.randomUUID()
class Bde_particles : ModInitializer { class Bde_particles : ModInitializer {
companion object {
val GLOBAL_MAX_PARTICLES: GameRules.Key<GameRules.IntRule> = GameRuleRegistry.register(
"GlobalMaxParticles",
GameRules.Category.UPDATES,
GameRuleFactory.createIntRule(3000, 0)
)
val SHOW_PARTICLE_DEBUG: GameRules.Key<GameRules.BooleanRule> = GameRuleRegistry.register(
"ShowParticleDebug",
GameRules.Category.UPDATES,
GameRuleFactory.createBooleanRule(false)
)
}
override fun onInitialize() { override fun onInitialize() {
if (FabricLoader.getInstance().environmentType != EnvType.SERVER) { if (FabricLoader.getInstance().environmentType != EnvType.SERVER) {
return return
@@ -39,18 +84,15 @@ class Bde_particles : ModInitializer {
println("BPS - Initializing on ${FabricLoader.getInstance().environmentType}") println("BPS - Initializing on ${FabricLoader.getInstance().environmentType}")
ServerLifecycleEvents.SERVER_STARTED.register { _ ->
init()
}
ServerTickEvents.START_SERVER_TICK.register { server -> ServerTickEvents.START_SERVER_TICK.register { server ->
tick(server) tick(server)
} }
CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, _ -> CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, _ ->
GiveEmitterTool.register(dispatcher, registryAccess) GiveEmitterTool.register(dispatcher, registryAccess)
ManageEmitters .register(dispatcher) ManageEmitters .register(dispatcher, registryAccess)
KillAllEmitters.register(dispatcher) KillAllEmitters.register(dispatcher)
SpawnEmitter .register(dispatcher)
} }
UseItemCallback.EVENT.register(UseItemCallback { player: PlayerEntity, world: World, hand: Hand -> UseItemCallback.EVENT.register(UseItemCallback { player: PlayerEntity, world: World, hand: Hand ->
@@ -61,6 +103,8 @@ class Bde_particles : ModInitializer {
}) })
ServerLifecycleEvents.SERVER_STARTED.register(ServerLifecycleEvents.ServerStarted { _ -> ServerLifecycleEvents.SERVER_STARTED.register(ServerLifecycleEvents.ServerStarted { _ ->
init()
println("BPS - Progress bar filled :3") println("BPS - Progress bar filled :3")
println( println(
"\n____________ _____ _____ \n" + "\n____________ _____ _____ \n" +
@@ -76,31 +120,41 @@ class Bde_particles : ModInitializer {
fun init() { fun init() {
addToEmitterRegister("DEFAULT", EmitterParams.DEFAULT) addToRegister("DEFAULT", EmitterParams.DEFAULT)
addToEmitterRegister("FIRE_GEYSER", EmitterParams.FIRE_GEYSER) addToRegister("DEBUG", EmitterParams.DEBUG)
addToEmitterRegister("RING_EXPLOSION", EmitterParams.RING_EXPLOSION) addToRegister("STRESS_TEST", EmitterParams.STRESS_TEST)
} }
fun tick(server: MinecraftServer) { fun tick(server: MinecraftServer) {
for (world in server.worlds) { runBlocking {
for (entity in world.iterateEntities()) { server.worlds.map { world ->
if (entity.type == EntityType.BLOCK_DISPLAY) { async {
if (displayEntityContainsTag(entity as BlockDisplayEntity, "BPS_UUID") val displayEntities = world.iterateEntities()
&& !displayEntityContainsTag(entity, sessionUuid.toString())) { .filter { it.type == EntityType.ITEM_DISPLAY }
for (entity in displayEntities) {
if (displayEntityContainsTag(entity as ItemDisplayEntity, "BPS_UUID")
&& !displayEntityContainsTag(entity, SESSION_UUID.toString())) {
entity.kill() entity.kill()
} }
} }
} }
}.awaitAll()
} }
for (emitter in ALL_PARTICLE_EMITTERS) { runBlocking {
ALL_PARTICLE_EMITTERS.map { emitter ->
async {
if (emitter.isDead) { if (emitter.isDead) {
synchronized(ALL_PARTICLE_EMITTERS) {
ALL_PARTICLE_EMITTERS = ALL_PARTICLE_EMITTERS.toMutableList().apply { remove(emitter) }.toTypedArray() ALL_PARTICLE_EMITTERS = ALL_PARTICLE_EMITTERS.toMutableList().apply { remove(emitter) }.toTypedArray()
} }
}
else { emitter.tick() } else { emitter.tick() }
} }
}.awaitAll()
}
} }
@@ -108,7 +162,7 @@ fun onRightClick(player: PlayerEntity, world: ServerWorld, hand: Hand) : TypedAc
val handItem = player.getStackInHand(hand) val handItem = player.getStackInHand(hand)
val (isEmitterTool, emitterId) = ParticleEmitterTool.getToolDetails(handItem) val (isEmitterTool, emitterId) = ParticleEmitterTool.getToolDetails(handItem)
val hitResult = raycastFromPlayer(player as ServerPlayerEntity, 200.0) val hitResult = raycastFromPlayer(player as ServerPlayerEntity, 200.0)
val emitterParams = getEmitterParams(emitterId) val emitterParams = getEmitterDataById(emitterId)
if ( if (
!isEmitterTool !isEmitterTool
@@ -118,53 +172,77 @@ fun onRightClick(player: PlayerEntity, world: ServerWorld, hand: Hand) : TypedAc
return TypedActionResult(ActionResult.PASS, handItem) return TypedActionResult(ActionResult.PASS, handItem)
} }
val emitter = ParticleEmitter(hitResult.pos, world, emitterParams) val debug = world.gameRules.getBoolean(Bde_particles.SHOW_PARTICLE_DEBUG)
ALL_PARTICLE_EMITTERS += emitter
val emitter = ParticleEmitter(
hitResult.pos,
Vector2f(0.0f, 0.0f),
world,
emitterParams,
debug
)
spawnEmitter(emitter)
return TypedActionResult(ActionResult.PASS, handItem) return TypedActionResult(ActionResult.PASS, handItem)
} }
fun emitterParamsToJson(params: EmitterParams) : Map<String, Any> { fun displayEntityContainsTag(entity: ItemDisplayEntity, tag: String) : Boolean {
val allParticleParamsJSON: MutableList<Map<String, Any?>> = mutableListOf()
val particle = params.particle
val particleParamsJSON = mapOf(
"shape" to particle.shape,
"blockCurve" to particle.blockCurve,
"rotRandom" to particle.rotRandom,
"rotVelRandom" to particle.rotVelRandom,
"rotVelCurve" to particle.rotVelCurve,
"sizeRandom" to particle.sizeRandom,
"uniformSize" to particle.uniformSize,
"sizeCurve" to particle.sizeCurve,
"velRandom" to particle.velRandom,
"forceFields" to particle.forceFields,
"gravity" to particle.gravity,
"drag" to particle.drag,
"minVel" to particle.minVel,
"lifeTime" to particle.lifeTime
)
allParticleParamsJSON.add(particleParamsJSON)
val emitterParamsJSON = mapOf(
"maxCount" to params.maxCount,
"spawnsPerTick" to params.spawnsPerTick,
"loopDur" to params.loopDur,
"loopDelay" to params.loopDelay,
"loopCount" to params.loopCount,
"particleTypes" to allParticleParamsJSON
)
return emitterParamsJSON
}
fun displayEntityContainsTag(entity: BlockDisplayEntity, tag: String) : Boolean {
val nbt = NbtCompound() val nbt = NbtCompound()
entity.writeNbt(nbt) entity.writeNbt(nbt)
val tagsNbtList = nbt.getList("Tags", 8) val tagsNbtList = nbt.getList("Tags", 8)
return tagsNbtList?.any { it.asString() == tag } == true return tagsNbtList?.any { it.asString() == tag } == true
} }
fun emitterListArg(argName: String) : RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument(argName, StringArgumentType.string())
.suggests { _, builder ->
idRegister.keys.forEach { key ->
builder.suggest(key)
}
CompletableFuture.completedFuture(builder.build())
}
}
fun curveListArg(argName: String): RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument(argName, StringArgumentType.string())
.suggests { _, builder ->
LerpCurves.Companion::class.members
.filterIsInstance<kotlin.reflect.KProperty1<LerpCurves.Companion, *>>()
.filter { it.returnType.classifier == LerpCurves::class }
.forEach { property ->
builder.suggest(property.name)
}
CompletableFuture.completedFuture(builder.build())
}
}
fun stringToCurve(curveName: String): LerpCurves? {
val companion = LerpCurves::class.companionObjectInstance
val properties = LerpCurves::class.companionObject?.declaredMemberProperties
return properties?.firstOrNull { it.name == curveName }?.getter?.call(companion) as? LerpCurves
}
fun spawnEmitter(emitter: ParticleEmitter) {
ALL_PARTICLE_EMITTERS += emitter
}
fun positiveFeedback(text: String, context: CommandContext<ServerCommandSource>) {
val feedback = Text.literal("BPS - $text")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
context.source.sendFeedback({ feedback }, false)
}
fun negativeFeedback(text: String, context: CommandContext<ServerCommandSource>) {
val feedback = Text.literal("BPS - $text")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
context.source.sendFeedback({ feedback }, false)
}
@@ -1,21 +1,21 @@
package com.bytedice.bde_particles package com.bytedice.bde_particles
import net.minecraft.entity.EntityType import net.minecraft.entity.EntityType
import net.minecraft.entity.decoration.DisplayEntity.BlockDisplayEntity import net.minecraft.entity.decoration.DisplayEntity.ItemDisplayEntity
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtFloat import net.minecraft.nbt.NbtFloat
import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtString import net.minecraft.nbt.NbtString
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
class DisplayEntity(private var properties: DisplayEntityProperties?) { class DisplayEntity(var properties: DisplayEntityProperties?) {
private var hasSpawned = false private var hasSpawned = false
private var entity: BlockDisplayEntity? = null private var entity: ItemDisplayEntity? = null
fun spawn(world: ServerWorld) { fun spawn(world: ServerWorld) {
if (hasSpawned || properties == null) { return } if (hasSpawned || properties == null) { return }
entity = BlockDisplayEntity(EntityType.BLOCK_DISPLAY, world) entity = ItemDisplayEntity(EntityType.ITEM_DISPLAY, world)
updateProperties(properties!!) updateProperties(properties!!)
world.spawnEntity(entity) world.spawnEntity(entity)
@@ -34,7 +34,13 @@ class DisplayEntity(private var properties: DisplayEntityProperties?) {
put("Tags", tagList) put("Tags", tagList)
put("block_state", NbtCompound().apply { putString("Name", properties.blockType) }) if (properties.model != "none") {
put("item", NbtCompound().apply {
putString("id", properties.model)
putInt("count", 1)
put("components", NbtCompound().apply { putInt("minecraft:custom_model_data", properties.customModel) })
})
}
put("transformation", NbtCompound().apply { put("transformation", NbtCompound().apply {
val offsetList = NbtList().apply { val offsetList = NbtList().apply {
@@ -65,6 +71,22 @@ class DisplayEntity(private var properties: DisplayEntityProperties?) {
put("scale", scaleList) put("scale", scaleList)
put("right_rotation", rightRotList) put("right_rotation", rightRotList)
}) })
putFloat("view_range", properties.viewRange)
putString("billboard", properties.billboard)
if (properties.customName != null) {
putString("CustomName", "{\"text\":\"${properties.customName}\"}")
putByte("CustomNameVisible", 1)
}
if (properties.brightnessOverride != null) {
put("brightness", NbtCompound().apply {
putInt("block", properties.brightnessOverride!!)
putInt("sky", properties.brightnessOverride!!)
})
}
} }
entity?.readNbt(nbt) entity?.readNbt(nbt)
@@ -75,7 +97,7 @@ class DisplayEntity(private var properties: DisplayEntityProperties?) {
fun kill() { fun kill() {
if (entity == null) { println("BPS - Particle Emitter entity is null!"); return } if (entity == null) { println("BPS - Failed to kill BDE. Entity is null!"); return }
entity?.kill() entity?.kill()
} }
} }
@@ -6,14 +6,28 @@ import org.joml.Vector3f
import org.joml.Vector4f import org.joml.Vector4f
data class DisplayEntityProperties ( data class DisplayEntityProperties (
val pos: Vec3d = Vec3d(0.0, 0.0, 0.0), var pos: Vec3d = Vec3d(0.0, 0.0, 0.0),
val rot: Vector2f = Vector2f(0.0f, 0.0f), var rot: Vector2f = Vector2f(0.0f, 0.0f),
val blockType: String = "minecraft:purple_concrete", var model: String = "minecraft:purple_concrete",
val tags: Array<String> = emptyArray(), var customModel: Int = 0,
var tags: Array<String> = emptyArray(),
var viewRange: Float = 2.0f,
var customName: String? = null,
var brightnessOverride: Int? = null,
var billboard: String = Billboard.FIXED,
val translation: Vector3f = Vector3f(-0.5f, -0.5f, -0.5f), var translation: Vector3f = Vector3f(0.0f, 0.0f, 0.0f),
val leftRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f), var leftRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
val scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), var scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f),
val rightRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f) var rightRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f)
) )
class Billboard () {
companion object {
const val FIXED: String = "fixed"
const val VERTICAL: String = "vertical"
const val HORIZONTAL: String = "horizontal"
const val CENTER: String = "center"
}
}
@@ -5,25 +5,24 @@ import net.minecraft.util.hit.HitResult
import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3d
import net.minecraft.world.RaycastContext import net.minecraft.world.RaycastContext
import net.minecraft.world.World import net.minecraft.world.World
import org.joml.Vector2f
import org.joml.Vector3f import org.joml.Vector3f
import org.joml.Vector4f import org.joml.Vector4f
import kotlin.math.* import kotlin.math.*
import kotlin.random.Random import kotlin.random.Random
class InterpolationCurves(val function: (Float) -> Float) { class LerpCurves(val function: (Float) -> Float) {
companion object { companion object {
val LINEAR = InterpolationCurves { y -> y } val Linear = LerpCurves { y -> y }
val SQRT = InterpolationCurves { y -> sqrt(y) } val Sqrt = LerpCurves { y -> sqrt(y) }
val EXPONENT = InterpolationCurves { y -> y.pow(2.0f) } val Exponent = LerpCurves { y -> y.pow(2.0f) }
val CUBIC = InterpolationCurves { y -> y.pow(3.0f) } val Cubic = LerpCurves { y -> y.pow(3.0f) }
val SINE = InterpolationCurves { y -> sin(y * PI.toFloat() / 2) } val Sine = LerpCurves { y -> sin(y * PI.toFloat() / 2) }
val COSINE = InterpolationCurves { y -> 1 - cos(y * PI.toFloat() / 2) } val Cosine = LerpCurves { y -> 1 - cos(y * PI.toFloat() / 2) }
val INVERSE = InterpolationCurves { y -> 1 - y } val Inverse = LerpCurves { y -> 1 - y }
val LOG = InterpolationCurves { y -> if (y > 0) ln(y + 1) else 0f } val Log = LerpCurves { y -> if (y > 0) ln(y + 1) else 0f }
val EXP = InterpolationCurves { y -> exp(y) - 1 } val Exp = LerpCurves { y -> exp(y) - 1 }
val BOUNCE = InterpolationCurves { y -> val Bounce = LerpCurves { y ->
val n1 = 7.5625f val n1 = 7.5625f
val d1 = 2.75f val d1 = 2.75f
when { when {
@@ -43,7 +42,11 @@ class InterpolationCurves(val function: (Float) -> Float) {
} }
} }
fun custom(equation: (Float) -> Float) = InterpolationCurves(equation) fun custom(equation: (Float) -> Float) = LerpCurves(equation)
}
fun deepCopy() : LerpCurves {
return LerpCurves(this.function)
} }
} }
@@ -73,28 +76,23 @@ fun raycastFromPlayer(player: ServerPlayerEntity, maxDistance: Double): HitResul
fun randomFloatBetween(min: Float, max: Float) : Float { fun randomFloatBetween(min: Float, max: Float) : Float {
return Random.nextFloat() * (max - min) + min return if (min == max) { max }
else if (min > max) { Random.nextFloat() * (min - max) + max }
else { Random.nextFloat() * (max - min) + min }
} }
fun randomIntBetween(min: Int, max: Int) : Int { fun randomIntBetween(min: Int, max: Int) : Int {
return Random.nextInt(min, max) return if (min == max) { max }
} else if (min > max) { Random.nextInt(min - max) + max }
else { Random.nextInt(max - min) + min }
fun randomBetweenVector3f(min: Vector3f, max: Vector3f) : Vector3f {
return Vector3f(
randomFloatBetween(min.x, max.x),
randomFloatBetween(min.y, max.y),
randomFloatBetween(min.z, max.z)
)
} }
fun eulerToQuat(euler: Vector3f): Vector4f { fun eulerToQuat(euler: Vector3f): Vector4f {
val roll = euler.x val roll = euler.x * (Math.PI / 180).toFloat()
val pitch = euler.y val pitch = euler.y * (Math.PI / 180).toFloat()
val yaw = euler.z val yaw = euler.z * (Math.PI / 180).toFloat()
val cy = cos(yaw * 0.5) val cy = cos(yaw * 0.5)
val sy = sin(yaw * 0.5) val sy = sin(yaw * 0.5)
@@ -137,45 +135,6 @@ fun quatToEuler(quat: Vector4f): Vector3f {
} }
fun randomInCircle(r: Float) : Vector2f {
val randRadius = r * sqrt(randomFloatBetween(0.0f, 1.0f))
val randAngle = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
val randomPos = Vector2f(randRadius * cos(randAngle), randRadius * sin(randAngle))
return randomPos
}
fun randomInSphere(r: Float) : Vector3f {
val randRadius = r * Random.nextDouble().pow(1.0 / 3.0)
val phi = Random.nextDouble(0.0, 2 * Math.PI)
val cosTheta = Random.nextDouble(-1.0, 1.0)
val theta = acos(cosTheta)
val x = randRadius * sin(theta) * cos(phi)
val y = randRadius * sin(theta) * sin(phi)
val z = randRadius * cos(theta)
return Vector3f(x.toFloat(), y.toFloat(), z.toFloat())
}
fun randomInRect(size: Vector2f) : Vector2f {
return Vector2f(randomFloatBetween(0.0f, size.x), randomFloatBetween(0.0f, size.y))
}
fun randomInCube(size: Vector3f) : Vector3f {
return Vector3f(
randomFloatBetween(0.0f, size.x),
randomFloatBetween(0.0f, size.y),
randomFloatBetween(0.0f, size.z)
)
}
fun transformOffsetByQuat(offset: Vector3f, rotation: Vector4f): Vector3f { fun transformOffsetByQuat(offset: Vector3f, rotation: Vector4f): Vector3f {
val x = offset.x val x = offset.x
val y = offset.y val y = offset.y
@@ -205,22 +164,14 @@ fun transformOffsetByScale(offset: Vector3f, scale: Vector3f): Vector3f {
fun lerp(x: Float, y: Float, t: Float) : Float { fun lerp(x: Float, y: Float, t: Float) : Float {
return x + (y - x) * t val clampedT = t.coerceIn(0f, 1f)
return x + (y - x) * clampedT
} }
fun interpolateCurve(start: Vector3f, end: Vector3f, t: Float, curve: InterpolationCurves): Vector3f { fun lerpArray(array: Array<Any>, t: Float, curve: LerpCurves = LerpCurves.Linear) : Any {
val clampedT = t.coerceIn(0.0f, 1.0f) val idx = round(lerp(0.0f, array.lastIndex.toFloat(), t) * curve.function(t)).toInt()
return array[idx.coerceIn(0, array.lastIndex)]
val curvedT = curve.function(clampedT)
val interpolatedVec = Vector3f(
lerp(start.x, end.x, curvedT),
lerp(start.y, end.y, curvedT),
lerp(start.z, end.z, curvedT)
)
return interpolatedVec
} }
@@ -234,7 +185,6 @@ fun sdfSphere(pos: Vector3f, radius: Float, objectPos: Vector3f): Float {
} }
fun sdfCube(pos: Vector3f, size: Vector3f, objectPos: Vector3f): Float { fun sdfCube(pos: Vector3f, size: Vector3f, objectPos: Vector3f): Float {
val px = objectPos.x - pos.x val px = objectPos.x - pos.x
val py = objectPos.y - pos.y val py = objectPos.y - pos.y
@@ -0,0 +1,914 @@
@file:Suppress("UNCHECKED_CAST")
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.curveListArg
import com.bytedice.bde_particles.negativeFeedback
import com.bytedice.bde_particles.particles.*
import com.bytedice.bde_particles.positiveFeedback
import com.bytedice.bde_particles.stringToCurve
import com.mojang.brigadier.Command
import com.mojang.brigadier.arguments.BoolArgumentType
import com.mojang.brigadier.arguments.FloatArgumentType
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.registry.Registries
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import org.joml.Vector2f
import org.joml.Vector3f
import kotlin.reflect.*
import kotlin.reflect.full.memberProperties
typealias CommandArgumentBuilder = (
access: KProperty1<EmitterParams, *>,
config: LiteralArgumentBuilder<ServerCommandSource>,
reg: CommandRegistryAccess
) -> LiteralArgumentBuilder<ServerCommandSource>?
fun listConfigArgs(configArgs: Array<String>,
rootArg: RequiredArgumentBuilder<ServerCommandSource, *>,
registryAccess: CommandRegistryAccess
): RequiredArgumentBuilder<ServerCommandSource, *>
{
configArgs.forEach { arg ->
var configArg = CommandManager.literal(arg)
val argAccess = parseArgNameToAccess(arg)!!
val func = parseArgTypeToFunc(argAccess.returnType, configArg)
if (func != null) {
configArg = func(argAccess, configArg, registryAccess)
}
rootArg.then(configArg)
}
return rootArg
}
fun dataClassToArray(dataClass: KClass<*>): Array<String> {
return dataClass.memberProperties.map { it.name }.toTypedArray()
}
fun parseArgNameToAccess(name: String) : KProperty1<EmitterParams, *>? {
val access = EmitterParams::class.memberProperties
.firstOrNull { it.name == name }
return access
}
fun parseArgTypeToFunc(type: KType, configArg: LiteralArgumentBuilder<ServerCommandSource>): CommandArgumentBuilder? {
return when (type.classifier) {
Int::class -> { access, _, _ ->
configArg.then(intArg(access as KProperty1<EmitterParams, Int>))
}
Float::class -> { access, _, _ ->
configArg.then(floatArg(access as KProperty1<EmitterParams, Float>))
}
String::class -> { access, _, _ ->
configArg.then(stringArg(access as KProperty1<EmitterParams, String>))
}
ParamClasses.Duration::class -> { access, config, _ ->
durArg(access as KProperty1<EmitterParams, ParamClasses.Duration>, config)
}
Vector3f::class -> { access, _, _ ->
configArg.then(vec3fArg(access as KProperty1<EmitterParams, Vector3f>))
}
ParamClasses.PairInt::class -> { access, _, _ ->
configArg.then(pairIntArg(access as KProperty1<EmitterParams, ParamClasses.PairInt>))
}
SpawningShape::class -> { access, config, _ ->
shapeArg(access as KProperty1<EmitterParams, SpawningShape>, config)
}
ParamClasses.PairVec3f::class -> { access, config, _ ->
pairVec3fArg(access as KProperty1<EmitterParams, ParamClasses.PairVec3f>, config)
}
ParamClasses.PairFloat::class -> { access, _, _ ->
configArg.then(pairFloatArg(access as KProperty1<EmitterParams, ParamClasses.PairFloat>))
}
ParamClasses.TransformWithVel::class -> { access, config, _ ->
transformWithVelArg(access as KProperty1<EmitterParams, ParamClasses.TransformWithVel>, config)
}
ParamClasses.StringCurve::class -> { access, config, reg ->
stringCurveArg(access as KProperty1<EmitterParams, ParamClasses.StringCurve>, config, reg)
}
ParamClasses.LerpValVec3f::class -> { access, config, _ ->
lerpValVec3fArg(access as KProperty1<EmitterParams, ParamClasses.LerpValVec3f>, config)
}
ParamClasses.ForceFieldArray::class -> { access, config, _ ->
forceFieldArg(access as KProperty1<EmitterParams, ParamClasses.ForceFieldArray>, config)
}
ParamClasses.LerpValInt::class -> { access, config, _ ->
lerpValIntArg(access as KProperty1<EmitterParams, ParamClasses.LerpValInt>, config)
}
else -> null
}
}
fun successText(access: KProperty1<EmitterParams, *>, value: String, id: String, context: CommandContext<ServerCommandSource>) {
positiveFeedback("Successfully updated parameter \"${access.name}\" to \"$value\" of Emitter ID \"$id\"", context)
}
fun intArg(access: KProperty1<EmitterParams, Int>) : RequiredArgumentBuilder<ServerCommandSource, Int> {
return CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = IntegerArgumentType.getInteger(context, "Int")
updateParam(id, access, value)
successText(access, value.toString(), id, context)
Command.SINGLE_SUCCESS
}
}
fun floatArg(access: KProperty1<EmitterParams, Float>) : RequiredArgumentBuilder<ServerCommandSource, Float> {
return CommandManager.argument("Float", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = FloatArgumentType.getFloat(context, "Float")
updateParam(id, access, value)
successText(access, value.toString(), id, context)
Command.SINGLE_SUCCESS
}
}
fun stringArg(access: KProperty1<EmitterParams, String>) : RequiredArgumentBuilder<ServerCommandSource, String> {
return CommandManager.argument("String", StringArgumentType.string())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = StringArgumentType.getString(context, "String")
updateParam(id, access, value)
successText(access, value.toString(), id, context)
Command.SINGLE_SUCCESS
}
}
fun durArg(access: KProperty1<EmitterParams, ParamClasses.Duration>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("SingleBurst")
.then(CommandManager.argument("loopDur", IntegerArgumentType.integer())
.executes { context -> durArgExec("SingleBurst", access, context); Command.SINGLE_SUCCESS }
)
)
.then(CommandManager.literal("MultiBurst")
.then(CommandManager.argument("loopDur", IntegerArgumentType.integer())
.then(CommandManager.argument("loopDelay", IntegerArgumentType.integer())
.then(CommandManager.argument("loopCount", IntegerArgumentType.integer())
.executes { context -> durArgExec("MultiBurst", access, context); Command.SINGLE_SUCCESS }
)
)
)
)
.then(CommandManager.literal("InfiniteLoop")
.then(CommandManager.argument("loopDur", IntegerArgumentType.integer())
.then(CommandManager.argument("loopDelay", IntegerArgumentType.integer())
.executes { context -> durArgExec("InfiniteLoop", access, context); Command.SINGLE_SUCCESS }
)
)
)
}
fun durArgExec(paramName: String, access: KProperty1<EmitterParams, ParamClasses.Duration>, context: CommandContext<ServerCommandSource>) {
val id = StringArgumentType.getString(context, "Emitter ID")
val loopDur = IntegerArgumentType.getInteger(context, "loopDur")
when (paramName) {
"SingleBurst" -> {
updateParam(id, access, ParamClasses.Duration.SingleBurst(loopDur))
}
"MultiBurst" -> {
val loopDelay = IntegerArgumentType.getInteger(context, "loopDelay")
val loopCount = IntegerArgumentType.getInteger(context, "loopCount")
updateParam(id, access, ParamClasses.Duration.MultiBurst(loopDur, loopDelay, loopCount))
}
"InfiniteLoop" -> {
val loopDelay = IntegerArgumentType.getInteger(context, "loopDelay")
updateParam(id, access, ParamClasses.Duration.InfiniteLoop(loopDur, loopDelay))
}
}
successText(access, access::class.simpleName ?: "UNKNOWN", id, context)
}
fun vec3fArg(access: KProperty1<EmitterParams, Vector3f>) : RequiredArgumentBuilder<ServerCommandSource, Float> {
return CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val valueX = FloatArgumentType.getFloat(context, "X")
val valueY = FloatArgumentType.getFloat(context, "Y")
val valueZ = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, Vector3f(valueX, valueY, valueZ))
successText(access, "[$valueX, $valueY, $valueZ]", id, context)
Command.SINGLE_SUCCESS
}
)
)
}
fun pairIntArg(access: KProperty1<EmitterParams, ParamClasses.PairInt>) : RequiredArgumentBuilder<ServerCommandSource, Int> {
return CommandManager.argument("Min Int", IntegerArgumentType.integer())
.then(CommandManager.argument("Max Int", IntegerArgumentType.integer())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val valueX = IntegerArgumentType.getInteger(context, "Min Int")
val valueY = IntegerArgumentType.getInteger(context, "Max Int")
updateParam(id, access, ParamClasses.PairInt(valueX, valueY))
successText(access, "[$valueX, $valueY]", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun shapeArg(access: KProperty1<EmitterParams, SpawningShape>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("Circle")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Circle", access, context); Command.SINGLE_SUCCESS }
)
)
)
.then(CommandManager.literal("Rect")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Rect", access, context); Command.SINGLE_SUCCESS }
)
)
)
)
.then(CommandManager.literal("Cube")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Cube", access, context); Command.SINGLE_SUCCESS }
)
)
)
)
)
.then(CommandManager.literal("Sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Spawn On Edge", BoolArgumentType.bool())
.executes { context -> shapeArgExec("Sphere", access, context); Command.SINGLE_SUCCESS }
)
)
)
.then(CommandManager.literal("Point")
.executes { context -> shapeArgExec("Point", access, context); Command.SINGLE_SUCCESS }
)
}
fun shapeArgExec(paramName: String, access: KProperty1<EmitterParams, SpawningShape>, context: CommandContext<ServerCommandSource>) {
val id = StringArgumentType.getString(context, "Emitter ID")
when (paramName) {
"Circle" -> {
val radius = FloatArgumentType.getFloat(context, "Radius")
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
updateParam(id, access, SpawningShape.Circle(radius, onEdge))
successText(access, "Circle", id, context)
}
"Rect" -> {
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
updateParam(id, access, SpawningShape.Rect(Vector2f(x, y), onEdge))
successText(access, "Rect", id, context)
}
"Cube" -> {
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, SpawningShape.Cube(Vector3f(x, y, z), onEdge))
successText(access, "Cube", id, context)
}
"Sphere" -> {
val radius = FloatArgumentType.getFloat(context, "Radius")
val onEdge = BoolArgumentType.getBool(context, "Spawn On Edge")
updateParam(id, access, SpawningShape.Sphere(radius, onEdge))
successText(access, "Sphere", id, context)
}
"Point" -> {
updateParam(id, access, SpawningShape.Point)
successText(access, "Point", id, context)
}
}
}
fun pairVec3fArg(access: KProperty1<EmitterParams,ParamClasses.PairVec3f>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("NonUniform")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val minX = FloatArgumentType.getFloat(context, "Min X")
val minY = FloatArgumentType.getFloat(context, "Min Y")
val minZ = FloatArgumentType.getFloat(context, "Min Z")
val maxX = FloatArgumentType.getFloat(context, "Max X")
val maxY = FloatArgumentType.getFloat(context, "Max Y")
val maxZ = FloatArgumentType.getFloat(context, "Max Z")
updateParam(id, access, ParamClasses.PairVec3f.NonUniform(minX, minY, minZ, maxX, maxY, maxZ))
successText(access, "NonUniform", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
.then(CommandManager.literal("Uniform")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val min = FloatArgumentType.getFloat(context, "X")
val max = FloatArgumentType.getFloat(context, "Y")
updateParam(id, access, ParamClasses.PairVec3f.Uniform(min, max))
successText(access, "Uniform", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
.then(CommandManager.literal("Constant")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, ParamClasses.PairVec3f.Constant(Vector3f(x, y, z)))
successText(access, "Constant", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.PairVec3f.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun pairFloatArg(access: KProperty1<EmitterParams, ParamClasses.PairFloat>) : RequiredArgumentBuilder<ServerCommandSource, Float> {
return CommandManager.argument("Min Float", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Float", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val valueX = FloatArgumentType.getFloat(context, "Min Float")
val valueY = FloatArgumentType.getFloat(context, "Max Float")
updateParam(id, access, ParamClasses.PairFloat(valueX, valueY))
successText(access, "[$valueX, $valueY]", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun transformWithVelArg(access: KProperty1<EmitterParams, ParamClasses.TransformWithVel>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("RotOnly")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.TransformWithVel.RotOnly)
successText(access, "RotOnly", id, context)
Command.SINGLE_SUCCESS
}
)
.then(CommandManager.literal("ScaleAndRot")
.then(CommandManager.argument("Scale Multiplier", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val scaleMul = FloatArgumentType.getFloat(context, "Scale Multiplier")
updateParam(id, access, ParamClasses.TransformWithVel.ScaleAndRot(scaleMul))
successText(access, "ScaleAndRot", id, context)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.TransformWithVel.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun stringCurveArg(access: KProperty1<EmitterParams, ParamClasses.StringCurve>,
configArg: LiteralArgumentBuilder<ServerCommandSource>,
registryAccess: CommandRegistryAccess
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("Add")
.then(CommandManager.argument("Item", ItemStackArgumentType.itemStack(registryAccess))
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
stringCurveAddArg(context, access, index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
stringCurveAddArg(context, access, null)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("Remove")
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
stringCurveRemoveArg(context, access, index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
stringCurveRemoveArg(context, access, null)
Command.SINGLE_SUCCESS
}
)
.then(CommandManager.literal("Curve")
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
val modelCurve = getEmitterDataById(id)?.modelCurve
if (curve != null && modelCurve != null) {
modelCurve.curve = curve
updateParam(id, access, modelCurve)
successText(access, curveName, id, context)
Command.SINGLE_SUCCESS
}
else {
negativeFeedback("Failed to update param \"modelCurve.curve\"! The curve is invalid!", context)
Command.SINGLE_SUCCESS
}
}
)
)
}
fun stringCurveAddArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.StringCurve>,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val item = ItemStackArgumentType.getItemStackArgument(context, "Item").item
val itemId = Registries.ITEM.getId(item).toString()
val modelCurve = getEmitterDataById(id)?.modelCurve
if (index == null && modelCurve != null) {
modelCurve.array = modelCurve.array.toMutableList().apply { add(itemId) }.toTypedArray()
updateParam(id, access, modelCurve)
}
else if (modelCurve != null) {
modelCurve.array = modelCurve.array.toMutableList().apply { add(index!!.coerceIn(0, modelCurve.array.lastIndex), itemId) }.toTypedArray()
updateParam(id, access, modelCurve)
}
successText(access, "++$item", id, context)
}
fun stringCurveRemoveArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.StringCurve>,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val modelCurve = getEmitterDataById(id)?.modelCurve
val item: String
if (index == null && modelCurve != null && modelCurve.array.isNotEmpty()) {
item = modelCurve.array.last()
modelCurve.array = modelCurve.array.toMutableList().apply { removeLast() }.toTypedArray()
updateParam(id, access, modelCurve)
}
else if (modelCurve != null && modelCurve.array.isNotEmpty()) {
item = modelCurve.array[index!!.coerceIn(0, modelCurve.array.lastIndex)]
modelCurve.array = modelCurve.array.toMutableList().apply { removeAt(index.coerceIn(0, modelCurve.array.lastIndex)) }.toTypedArray()
updateParam(id, access, modelCurve)
}
else {
negativeFeedback("Couldn't remove model. There are no models in the Emitter.", context)
return
}
successText(access, "--$item", id, context)
}
fun lerpValVec3fArg(access: KProperty1<EmitterParams,ParamClasses.LerpValVec3f>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("LerpVec3f")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val minX = FloatArgumentType.getFloat(context, "Min X")
val minY = FloatArgumentType.getFloat(context, "Min Y")
val minZ = FloatArgumentType.getFloat(context, "Min Z")
val maxX = FloatArgumentType.getFloat(context, "Max X")
val maxY = FloatArgumentType.getFloat(context, "Max Y")
val maxZ = FloatArgumentType.getFloat(context, "Max Z")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
updateParam(id, access, ParamClasses.LerpValVec3f.LerpVec3f(minX, minY, minZ, maxX, maxY, maxZ, curve!!))
successText(access, "LerpVec3f", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
)
.then(CommandManager.literal("MultiLerpVec3f")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(curveListArg("CurveX")
.then(curveListArg("CurveY")
.then(curveListArg("CurveZ")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val minX = FloatArgumentType.getFloat(context, "Min X")
val minY = FloatArgumentType.getFloat(context, "Min Y")
val minZ = FloatArgumentType.getFloat(context, "Min Z")
val maxX = FloatArgumentType.getFloat(context, "Max X")
val maxY = FloatArgumentType.getFloat(context, "Max Y")
val maxZ = FloatArgumentType.getFloat(context, "Max Z")
val curveNameX = StringArgumentType.getString(context, "CurveX")
val curveNameY = StringArgumentType.getString(context, "CurveY")
val curveNameZ = StringArgumentType.getString(context, "CurveZ")
val curveX = stringToCurve(curveNameX)
val curveY = stringToCurve(curveNameY)
val curveZ = stringToCurve(curveNameZ)
updateParam(id, access, ParamClasses.LerpValVec3f.MultiLerpVec3f(minX, minY, minZ, maxX, maxY, maxZ, curveX!!, curveY!!, curveZ!!))
successText(access, "MultiLerpVec3f", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
)
)
)
.then(CommandManager.literal("LerpUniform")
.then(CommandManager.argument("Min", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max", FloatArgumentType.floatArg())
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val min = FloatArgumentType.getFloat(context, "Min")
val max = FloatArgumentType.getFloat(context, "Max")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
updateParam(id, access, ParamClasses.LerpValVec3f.LerpUniform(min, max, curve!!))
successText(access, "LerpUniform", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Constant")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
updateParam(id, access, ParamClasses.LerpValVec3f.Constant(Vector3f(x, y, z)))
successText(access, "Constant", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.LerpValVec3f.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
fun forceFieldArg(access: KProperty1<EmitterParams,ParamClasses.ForceFieldArray>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("Add")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.then(CommandManager.literal("Sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
forceFieldAddArg(context, access, "Sphere", index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
forceFieldAddArg(context, access, "Sphere", null)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Cube")
.then(CommandManager.argument("Size X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Size Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Size Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Force X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Force Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Force Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
forceFieldAddArg(context, access, "Cube", index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
forceFieldAddArg(context, access, "Cube", null)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
)
)
)
)
.then(CommandManager.literal("Remove")
.then(CommandManager.argument("Index", IntegerArgumentType.integer())
.executes { context ->
val index = IntegerArgumentType.getInteger(context, "Index")
forceFieldRemoveArg(context, access, index)
Command.SINGLE_SUCCESS
}
)
.executes { context ->
forceFieldRemoveArg(context, access, null)
Command.SINGLE_SUCCESS
}
)
}
fun forceFieldAddArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.ForceFieldArray>,
shape: String,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
val forceFields = getEmitterDataById(id)?.forceFields
var forceField: ForceField? = null
if (shape == "Sphere") {
val radius = FloatArgumentType.getFloat(context, "Radius")
val minForce = FloatArgumentType.getFloat(context, "Min Force")
val maxForce = FloatArgumentType.getFloat(context, "Max Force")
forceField = ForceField(
Vector3f(x, y, z),
ForceFieldShape.Sphere(radius, Pair(minForce, maxForce))
)
}
else if (shape == "Cube") {
val sizeX = FloatArgumentType.getFloat(context, "Size X")
val sizeY = FloatArgumentType.getFloat(context, "Size Y")
val sizeZ = FloatArgumentType.getFloat(context, "Size Z")
val forceX = FloatArgumentType.getFloat(context, "Force X")
val forceY = FloatArgumentType.getFloat(context, "Force Y")
val forceZ = FloatArgumentType.getFloat(context, "Force Z")
forceField = ForceField(
Vector3f(x, y, z),
ForceFieldShape.Cube(Vector3f(sizeX, sizeY, sizeZ), Vector3f(forceX, forceY, forceZ))
)
}
if (forceFields != null && forceField != null) {
if (index == null) {
forceFields.array = forceFields.array.toMutableList().apply { add(forceField) }.toTypedArray()
}
else {
val newIndex = index.coerceIn(0, forceFields.array.size)
forceFields.array = forceFields.array.toMutableList().apply { add(newIndex, forceField) }.toTypedArray()
}
val calcIndex = index?.coerceIn(0, forceFields.array.size)
updateParam(id, access, forceFields)
successText(access, "++ForceField at $calcIndex", id, context)
}
else {
negativeFeedback("Failed to update param \"forceFields\"! The Emitter ID is most likely invalid!", context)
}
}
fun forceFieldRemoveArg(context: CommandContext<ServerCommandSource>,
access: KProperty1<EmitterParams, ParamClasses.ForceFieldArray>,
index: Int?
) {
val id = StringArgumentType.getString(context, "Emitter ID")
val forceFields = getEmitterDataById(id)?.forceFields
if (index == null && forceFields != null && forceFields.array.isNotEmpty()) {
forceFields.array = forceFields.array.toMutableList().apply { removeLast() }.toTypedArray()
updateParam(id, access, forceFields)
}
else if (forceFields != null && forceFields.array.isNotEmpty()) {
val newIndex = index?.coerceIn(0, forceFields.array.lastIndex) ?: forceFields.array.lastIndex
forceFields.array = forceFields.array.toMutableList().apply { removeAt(newIndex) }.toTypedArray()
updateParam(id, access, forceFields)
}
else {
negativeFeedback("Couldn't remove ForceField. There are no ForceFields in the Emitter.", context)
return
}
val calcIndex = index?.coerceIn(0, forceFields.array.size) ?: forceFields.array.size
successText(access, "--ForceField at $calcIndex", id, context)
}
fun lerpValIntArg(access: KProperty1<EmitterParams, ParamClasses.LerpValInt>,
configArg: LiteralArgumentBuilder<ServerCommandSource>
) : LiteralArgumentBuilder<ServerCommandSource>
{
return configArg
.then(CommandManager.literal("LerpInt")
.then(CommandManager.argument("From", IntegerArgumentType.integer())
.then(CommandManager.argument("To", IntegerArgumentType.integer())
.then(curveListArg("Curve")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val from = IntegerArgumentType.getInteger(context, "From")
val to = IntegerArgumentType.getInteger(context, "To")
val curveName = StringArgumentType.getString(context, "Curve")
val curve = stringToCurve(curveName)
updateParam(id, access, ParamClasses.LerpValInt.LerpInt(from, to, curve!!))
successText(access, "LerpInt", id, context)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("Constant")
.then(CommandManager.argument("Value", IntegerArgumentType.integer())
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
val value = IntegerArgumentType.getInteger(context, "Value")
updateParam(id, access, ParamClasses.LerpValInt.Constant(value))
successText(access, "Constant", id, context)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("Null")
.executes { context ->
val id = StringArgumentType.getString(context, "Emitter ID")
updateParam(id, access, ParamClasses.LerpValInt.Null)
successText(access, "Null", id, context)
Command.SINGLE_SUCCESS
}
)
}
@@ -1,12 +1,16 @@
package com.bytedice.bde_particles.commands package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.items.ParticleEmitterTool import com.bytedice.bde_particles.items.ParticleEmitterTool
import com.bytedice.bde_particles.particles.emitterIdRegister import com.bytedice.bde_particles.particles.idRegister
import com.mojang.brigadier.Command import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.CommandRegistryAccess import net.minecraft.command.CommandRegistryAccess
import net.minecraft.command.argument.ItemStackArgumentType import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.server.command.CommandManager import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.Style import net.minecraft.text.Style
@@ -18,9 +22,9 @@ import java.util.concurrent.CompletableFuture
object GiveEmitterTool { object GiveEmitterTool {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>, registryAccess: CommandRegistryAccess) { fun register(dispatcher: CommandDispatcher<ServerCommandSource>, registryAccess: CommandRegistryAccess) {
val command = CommandManager.literal("GiveEmitterTool") val command = CommandManager.literal("GiveEmitterTool")
.requires { source -> source.hasPermissionLevel(4) } .requires { source -> source.hasPermissionLevel(2) }
val allEmitterIds = emitterIdRegister.keys val allEmitterIds = idRegister.keys
val itemNameArg = CommandManager.argument("Item Name", StringArgumentType.string()) val itemNameArg = CommandManager.argument("Item Name", StringArgumentType.string())
val itemTypeArg = CommandManager.argument("Item Type", ItemStackArgumentType.itemStack(registryAccess)) val itemTypeArg = CommandManager.argument("Item Type", ItemStackArgumentType.itemStack(registryAccess))
@@ -33,26 +37,42 @@ object GiveEmitterTool {
CompletableFuture.completedFuture(builder.build()) CompletableFuture.completedFuture(builder.build())
} }
dispatcher.register( dispatcher.register(command
command
.then(itemNameArg
.then(itemTypeArg
.then(emitterIdSuggestion .then(emitterIdSuggestion
.then(itemTypeArg
.then(itemNameArg
.executes { context -> .executes { context ->
val name = StringArgumentType.getString(context, "Item Name") val name = StringArgumentType.getString(context, "Item Name")
val item = ItemStackArgumentType.getItemStackArgument(context, "Item Type") val item = ItemStackArgumentType.getItemStackArgument(context, "Item Type")
val emitterId = StringArgumentType.getString(context, "Emitter ID") val emitterId = StringArgumentType.getString(context, "Emitter ID")
val feedback = Text.literal("BPS - You like fancy particles. Don't you?\nBPS - gave particle emitter, bound to ID: \"${emitterId}\".") exec(context, emitterId, item.item, name)
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb))) Command.SINGLE_SUCCESS
}
)
.executes { context ->
val item = ItemStackArgumentType.getItemStackArgument(context, "Item Type")
val emitterId = StringArgumentType.getString(context, "Emitter ID")
context.source.player?.inventory?.insertStack(ParticleEmitterTool.makeData(item.item, name, emitterId)) exec(context, emitterId, item.item, emitterId)
context.source.sendFeedback({ feedback }, false) Command.SINGLE_SUCCESS
}
)
.executes { context ->
val emitterId = StringArgumentType.getString(context, "Emitter ID")
exec(context, emitterId, Items.AMETHYST_SHARD, emitterId)
Command.SINGLE_SUCCESS Command.SINGLE_SUCCESS
} }
) )
) )
) }
)
private fun exec(context: CommandContext<ServerCommandSource>, emitterId: String, itemType: Item, itemName: String) {
val feedback = Text.literal("BPS - You like fancy particles. Don't you?\nBPS - gave particle emitter, bound to ID: \"${emitterId}\".")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
context.source.player?.inventory?.insertStack(ParticleEmitterTool.makeData(itemType, itemName, emitterId))
context.source.sendFeedback({ feedback }, false)
} }
} }
@@ -16,11 +16,13 @@ object KillAllEmitters {
dispatcher.register( dispatcher.register(
command.executes { context -> command.executes { context ->
var amountEmitters = 0
for (emitter in ALL_PARTICLE_EMITTERS) { for (emitter in ALL_PARTICLE_EMITTERS) {
emitter.kill() emitter.kill()
amountEmitters += 1
} }
val feedback = Text.literal("BPS - Killed all living Emitters.") val feedback = Text.literal("BPS - Killed all living Emitters. [$amountEmitters]")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb)) .setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
context.source.sendFeedback({ feedback }, false) context.source.sendFeedback({ feedback }, false)
@@ -1,602 +0,0 @@
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.InterpolationCurves
import com.bytedice.bde_particles.particles.*
import com.mojang.brigadier.Command
import com.mojang.brigadier.arguments.BoolArgumentType
import com.mojang.brigadier.arguments.FloatArgumentType
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.MutableText
import net.minecraft.text.Style
import net.minecraft.text.Text
import org.joml.Vector2f
import org.joml.Vector3f
import java.awt.Color
import java.util.concurrent.CompletableFuture
// CLOSE THIS FILE IMMEDIATELY, YOU ARE ENTERING EGYPTIAN (pyramid) AND ITALIAN (spaghetti) TERRITORY
val curves = arrayOf("LINEAR", "SQRT", "EXPONENT", "CUBIC", "SINE", "COSINE", "INVERSE", "LOG", "EXP", "BOUNCE")
fun updateEmitterParam(context: CommandContext<ServerCommandSource>, paramName: String, typeValue: Any) : MutableText {
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val success = updateSingleEmitterParam(emitterIdVal, paramName, typeValue)
val feedback = if (success) {
Text.literal("BPS - Successfully updated Emitter ID \"$emitterIdVal\": Parameter \"$paramName\", value \"$typeValue\"")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
}
else {
Text.literal("BPS - Failed to update Emitter ID \"$emitterIdVal\": Parameter \"$paramName\", value \"$typeValue\": Unknown Error\n" +
"Try checking if the emitter and parameter exist, and the value is the correct type.")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
}
return feedback
}
fun updateParticleParam(context: CommandContext<ServerCommandSource>, paramName: String, typeValue: Any) : MutableText {
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val success = updateSingleParticleParam(emitterIdVal, paramName, typeValue)
val feedback = if (success) {
Text.literal("BPS - Successfully updated Particle of Emitter ID \"$emitterIdVal\": parameter \"$paramName\", value \"$typeValue\"")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
}
else {
Text.literal("BPS - Failed to update Emitter ID \"$emitterIdVal\": parameter \"$paramName\", value \"$typeValue\": Unknown Error\n" +
"Try checking if the Emitter ID and parameter exist, and the value is the correct type.")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
}
return feedback
}
class ArgConfigEmitterKeys {
fun maxCount() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("maxCount")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "maxCount", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun spawnsPerTick() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("spawnsPerTick")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "spawnsPerTick", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun loopDur() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("loopDur")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "loopDur", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun loopDelay() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("loopDelay")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "loopDelay", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun loopCount() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("loopCount")
.then(CommandManager.argument("Int", IntegerArgumentType.integer())
.executes { context ->
val typeValue = IntegerArgumentType.getInteger(context, "Int")
val feedback = updateEmitterParam(context, "loopCount", typeValue)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
}
class ArgConfigParticleKeys {
fun shape() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("shape")
.then(CommandManager.literal("circle")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.executes { context ->
val radius = FloatArgumentType.getFloat(context, "Radius")
val feedback = updateParticleParam(context, "shape", SpawningShape.CIRCLE(radius))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("rect")
.then(CommandManager.argument("Width", FloatArgumentType.floatArg())
.then(CommandManager.argument("Height", FloatArgumentType.floatArg())
.executes { context ->
val w = FloatArgumentType.getFloat(context, "Width")
val h = FloatArgumentType.getFloat(context, "Height")
val feedback = updateParticleParam(context, "shape", SpawningShape.RECT(Vector2f(w, h)))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
.then(CommandManager.literal("cube")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val x = FloatArgumentType.getFloat(context, "X")
val y = FloatArgumentType.getFloat(context, "Y")
val z = FloatArgumentType.getFloat(context, "Z")
val feedback = updateParticleParam(context, "shape", SpawningShape.CUBE(Vector3f(x, y, z)))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
.then(CommandManager.literal("sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.executes { context ->
val radius = FloatArgumentType.getFloat(context, "Radius")
val feedback = updateParticleParam(context, "shape", SpawningShape.SPHERE(radius))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
.then(CommandManager.literal("point")
.executes { context ->
val feedback = updateParticleParam(context, "shape", SpawningShape.POINT)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun blockCurve() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("blockCurve")
.then(CommandManager.argument("Array (separate by comma)", StringArgumentType.string())
.executes { context ->
val typeValue = StringArgumentType.getString(context, "Array (separate by comma)")
val typeArray = typeValue.split(",").toTypedArray()
val feedback = updateParticleParam(context, "blockCurve", typeArray)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun rotRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("rotRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "rotRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun rotVelRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("rotVelRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "rotVelRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun sizeRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("sizeRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "sizeRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun uniformSize() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("uniformSize")
.then(CommandManager.argument("Bool", BoolArgumentType.bool())
.executes { context ->
val uniform = BoolArgumentType.getBool(context, "Bool")
val feedback = updateParticleParam(context, "uniformSize", uniform)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun velRandom() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("velRandom")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val feedback = updateParticleParam(context, "velRandom", vectors)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
}
fun forceFields() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("forceFields")
.then(argAddForceField())
.then(argRemoveForceField())
}
fun gravity() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("gravity")
.then(CommandManager.argument("X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z", FloatArgumentType.floatArg())
.executes { context ->
val x1 = FloatArgumentType.getFloat(context, "X")
val y1 = FloatArgumentType.getFloat(context, "Y")
val z1 = FloatArgumentType.getFloat(context, "Z")
val feedback = updateParticleParam(context, "gravity", Vector3f(x1, y1, z1))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
}
fun drag() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("drag")
.then(CommandManager.argument("Float", FloatArgumentType.floatArg())
.executes { context ->
val value = FloatArgumentType.getFloat(context, "Float")
val feedback = updateParticleParam(context, "drag", value)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun minVel() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("minVel")
.then(CommandManager.argument("Float", FloatArgumentType.floatArg())
.executes { context ->
val value = FloatArgumentType.getFloat(context, "Float")
val feedback = updateParticleParam(context, "minVel", value)
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
}
fun lifeTime() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("lifeTime")
.then(CommandManager.argument("min", IntegerArgumentType.integer())
.then(CommandManager.argument("max", IntegerArgumentType.integer())
.executes { context ->
val min = IntegerArgumentType.getInteger(context, "min")
val max = IntegerArgumentType.getInteger(context, "max")
val feedback = updateParticleParam(context, "lifeTime", Pair(min, max))
context.source.sendFeedback( { feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
}
fun rotVelCurve() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("rotVelCurve")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Curve", StringArgumentType.string())
.suggests { _, builder ->
curves.forEach { builder.suggest(it) }
CompletableFuture.completedFuture(builder.build())
}
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val curveString = StringArgumentType.getString(context, "Curve")
val feedback = updateParticleParam(
context,
"rotVelCurve",
Triple(vectors.first, vectors.second, getCurveByString(curveString)))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
}
fun sizeCurve() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("sizeCurve")
.then(CommandManager.argument("Min X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max X", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Y", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Z", FloatArgumentType.floatArg())
.then(CommandManager.argument("Curve", StringArgumentType.string())
.suggests { _, builder ->
curves.forEach { builder.suggest(it) }
CompletableFuture.completedFuture(builder.build())
}
.executes { context ->
val vectors = x1x2ToPairVector3f(context)
val curveString = StringArgumentType.getString(context, "Curve")
val feedback = updateParticleParam(
context,
"sizeCurve",
Triple(vectors.first, vectors.second, getCurveByString(curveString)))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
)
)
)
)
)
)
)
}
}
fun getCurveByString(str: String) : InterpolationCurves {
return when (str) {
"LINEAR" -> InterpolationCurves.LINEAR
"SQRT" -> InterpolationCurves.SQRT
"EXPONENT" -> InterpolationCurves.EXPONENT
"CUBIC" -> InterpolationCurves.CUBIC
"SINE" -> InterpolationCurves.SINE
"COSINE" -> InterpolationCurves.COSINE
"INVERSE" -> InterpolationCurves.INVERSE
"LOG" -> InterpolationCurves.LOG
"EXP" -> InterpolationCurves.EXP
"BOUNCE" -> InterpolationCurves.BOUNCE
else -> InterpolationCurves.LINEAR
}
}
fun x1x2ToPairVector3f(context: CommandContext<ServerCommandSource>) : Pair<Vector3f, Vector3f> {
val x1 = FloatArgumentType.getFloat(context, "Min X")
val y1 = FloatArgumentType.getFloat(context, "Min Y")
val z1 = FloatArgumentType.getFloat(context, "Min Z")
val x2 = FloatArgumentType.getFloat(context, "Max X")
val y2 = FloatArgumentType.getFloat(context, "Max Y")
val z2 = FloatArgumentType.getFloat(context, "Max Z")
return Pair(Vector3f(x1, y1, z1), Vector3f(x2, y2, z2))
}
fun argAddForceField() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("add")
.then(CommandManager.argument("Name", StringArgumentType.string())
.then(CommandManager.argument("X Pos", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y Pos", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z Pos", FloatArgumentType.floatArg())
.then(CommandManager.literal("sphere")
.then(CommandManager.argument("Radius", FloatArgumentType.floatArg())
.then(CommandManager.argument("Min Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Max Force", FloatArgumentType.floatArg())
.executes { context -> forceFieldAddSphereExec(context) }
)
)
)
)
.then(CommandManager.literal("cube")
.then(CommandManager.argument("X Size", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y Size", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z Size", FloatArgumentType.floatArg())
.then(CommandManager.argument("X Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Y Force", FloatArgumentType.floatArg())
.then(CommandManager.argument("Z Force", FloatArgumentType.floatArg())
.executes { context -> forceFieldAddCubeExec(context) }
)
)
)
)
)
)
)
)
)
)
)
}
fun argRemoveForceField() : LiteralArgumentBuilder<ServerCommandSource> { // TODO fix this error somehow
return CommandManager.literal("remove")
.then(CommandManager.argument("Force Field Name", StringArgumentType.string())
.suggests { context, builder ->
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val params = getEmitterParams(emitterIdVal)
val forceFields = params!!.particle.forceFields
for (forceField in forceFields) {
builder.suggest(forceField.name)
}
CompletableFuture.completedFuture(builder.build())
}
.executes { context ->
val ffName = StringArgumentType.getString(context, "Force Field Name")
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val params = getEmitterParams(emitterIdVal)
val forceFields = params!!.particle.forceFields
val newForceFields: MutableList<ForceField> = forceFields.toMutableList()
if (forceFields.isEmpty()) {
val feedback = Text.literal("BPS - There are no force fields for Emitter ID \"$emitterIdVal\".")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
else {
for (forceField in forceFields) {
if (forceField.name == ffName) {
newForceFields.apply { remove(forceField) }
}
}
val feedback = updateParticleParam(
context,
"forceFields",
newForceFields.toTypedArray()
)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
}
)
}
fun forceFieldAddSphereExec(context: CommandContext<ServerCommandSource>) : Int {
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val radius = FloatArgumentType.getFloat(context, "Radius")
val minForce = FloatArgumentType.getFloat(context, "Min Force")
val maxForce = FloatArgumentType.getFloat(context, "Max Force")
val shape = ForceFieldShape.SPHERE(radius, Pair(minForce, maxForce))
val forceField = forceFieldGenericParams(context, shape)
val currentParams = getEmitterParams(emitterIdVal)
val forceFields = currentParams!!.particle.forceFields
val addedArray = forceFields.toMutableList().apply { add(forceField) }.toTypedArray()
val feedback = updateParticleParam(
context,
"forceFields",
addedArray
)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
return Command.SINGLE_SUCCESS
}
fun forceFieldAddCubeExec(context: CommandContext<ServerCommandSource>) : Int {
val xSize = FloatArgumentType.getFloat(context, "X Size")
val ySize = FloatArgumentType.getFloat(context, "Y Size")
val zSize = FloatArgumentType.getFloat(context, "Z Size")
val xForce = FloatArgumentType.getFloat(context, "X Force")
val yForce = FloatArgumentType.getFloat(context, "Y Force")
val zForce = FloatArgumentType.getFloat(context, "Z Force")
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val shape = ForceFieldShape.CUBE(Vector3f(xSize, ySize, zSize), Vector3f(xForce, yForce, zForce))
val forceField = forceFieldGenericParams(context, shape)
val currentParams = getEmitterParams(emitterIdVal)
val forceFields = currentParams!!.particle.forceFields
val addedArray = forceFields.toMutableList().apply { add(forceField) }.toTypedArray()
val feedback = updateParticleParam(
context,
"forceFields",
addedArray
)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
return Command.SINGLE_SUCCESS
}
fun forceFieldGenericParams(context: CommandContext<ServerCommandSource>, shape: ForceFieldShape) : ForceField {
val name = StringArgumentType.getString(context, "Name")
val posX = FloatArgumentType.getFloat(context, "X Pos")
val posY = FloatArgumentType.getFloat(context, "Y Pos")
val posZ = FloatArgumentType.getFloat(context, "Z Pos")
return ForceField(
name.replace(" ", "_"),
Vector3f(posX, posY, posZ),
shape
)
}
@@ -1,30 +1,58 @@
package com.bytedice.bde_particles.commands package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.emitterParamsToJson import com.bytedice.bde_particles.emitterListArg
import com.bytedice.bde_particles.negativeFeedback
import com.bytedice.bde_particles.particles.* import com.bytedice.bde_particles.particles.*
import com.bytedice.bde_particles.positiveFeedback
import com.mojang.brigadier.Command import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder
import net.minecraft.component.DataComponentTypes import com.mojang.brigadier.context.CommandContext
import net.minecraft.component.type.NbtComponent import net.minecraft.command.CommandRegistryAccess
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.command.CommandManager import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.MutableText
import net.minecraft.text.Style import net.minecraft.text.Style
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.text.TextColor
import java.awt.Color import java.awt.Color
import java.util.concurrent.CompletableFuture
// TODO: completely redesign
/*
// spawning
maxCount: Int,
spawnRate: Int,
spawnChance: Float,
spawnDuration: ParamClasses.Duration,
spawnPosOffset: Vector3f,
lifeTime: ParamClasses.PairInt,
shape: SpawningShape,
// init transforms
offset: ParamClasses.PairVec3f,
initRot: ParamClasses.PairVec3f,
rotVel: ParamClasses.PairVec3f,
rotWithVel: Boolean,
initVel: ParamClasses.PairVec3f,
initCenterVel: ParamClasses.PairFloat,
initScale: ParamClasses.PairVec3f,
scaleWithVel: Boolean,
// velocity
forceFields: Array<ForceField>,
constVel: Vector3f,
drag: Float,
minVel: Float,
// curves
offsetCurve: ParamClasses.LerpVal,
rotVelCurve: ParamClasses.LerpVal,
scaleCurve: ParamClasses.LerpVal,
blockCurve: Pair<Array<String>, LerpCurves>,
*/
// ManageEmitters // ManageEmitters
// <create / remove / config / list> // <create / remove / config / list / copy>
// create // create
// <emitter id> // <emitter id>
// <preset emitter id> // <preset emitter id>
@@ -36,9 +64,8 @@ import java.util.concurrent.CompletableFuture
// config // config
// <emitter id> // <emitter id>
// <PARTICLE / EMITTER> // <param key>
// <param key> // HARDCODED KEYS, if EMITTER use emitter params. // <new param value>
// <new param value> // HARDCODED TYPES
// output -> update the selected parameter of the selected emitter id to the new value // output -> update the selected parameter of the selected emitter id to the new value
// list // list
@@ -50,219 +77,121 @@ import java.util.concurrent.CompletableFuture
object ManageEmitters { object ManageEmitters {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>) {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>, registryAccess: CommandRegistryAccess) {
val command = CommandManager.literal("ManageEmitters") val command = CommandManager.literal("ManageEmitters")
.requires { source -> source.hasPermissionLevel(4) } .requires { source -> source.hasPermissionLevel(2) }
dispatcher.register( dispatcher.register(
command command
// <create / remove / config / list / copy> .then(CommandManager.literal("create").then(create()))
.then(argCreate()) .then(CommandManager.literal("remove").then(remove()))
.then(argRemove()) .then(CommandManager.literal("config").then(config(registryAccess)))
.then(argConfig()) .then(CommandManager.literal("list")
.then(argList())
.then(argCopy())
)
}
}
val allEmitterIds = emitterIdRegister.keys
// TODO: perhaps make this a Command.literal() if possible
val emitterIdSuggestion: RequiredArgumentBuilder<ServerCommandSource, String> = CommandManager.argument("Registered Emitter ID", StringArgumentType.string())
.suggests { _, builder ->
allEmitterIds.forEach { key ->
builder.suggest(key)
}
CompletableFuture.completedFuture(builder.build())
}
val emitterConfigKeys = ArgConfigEmitterKeys()
val particleConfigKeys = ArgConfigParticleKeys()
// create
// <emitter id>
// <preset emitter id>
// output -> register new emitter with a preset
fun argCreate() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("create")
.then(CommandManager.argument("Emitter ID", StringArgumentType.string())
.then(emitterIdSuggestion
.executes { context -> .executes { context ->
val emitterIdVal = StringArgumentType.getString(context, "Emitter ID") val emitterIdList = idRegister.keys.sorted().joinToString(" ")
val emitterPresetVal = StringArgumentType.getString(context, "Registered Emitter ID") ?: "DEFAULT"
val emitterPresetParams = getEmitterParams(emitterPresetVal)
var newEmitterId = "NULL" val feedback = Text.literal("List of all registered Emitter IDs:\n$emitterIdList")
val emitterRegisterData: Pair<String, Boolean>
if (emitterPresetParams != null) {
emitterRegisterData = addToEmitterRegister(emitterIdVal, emitterPresetParams)
newEmitterId = emitterRegisterData.first
}
else { emitterRegisterData = Pair("NULL", false) }
val feedback = if (emitterRegisterData.second) {
Text.literal("BPS - Created particle emitter with ID \"$newEmitterId\".\n" +
"BPS - This emitter will be removed on server restart! Use \"/ManageEmitters copy\" to save the data to your clipboard!")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
}
else if (newEmitterId !in allEmitterIds) {
Text.literal("BPS - Preset Emitter ID \"$emitterPresetVal\" doesn't exist!\n" +
"BPS - Use \"/ManageEmitters list\" to view all Emitter ID's.")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(200, 0, 0).rgb)))
}
else {
Text.literal("BPS - Emitter ID \"$newEmitterId\" already exists\n" +
"BPS - Use \"/ManageEmitters list\" to view all Emitter ID's.")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(200, 0, 0).rgb)))
}
context.source.sendFeedback( { feedback }, false )
Command.SINGLE_SUCCESS
}
)
)
}
// remove
// <emitter id>
// output -> removes the particle with that id
fun argRemove() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("remove")
.then(emitterIdSuggestion
.executes { context ->
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID") ?: "NULL"
val isRemoved = removeFromEmitterRegister(emitterIdVal)
val feedback = if (isRemoved) {
Text.literal("BPS - Removed particle emitter with ID \"$emitterIdVal\".")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
}
else {
Text.literal("BPS - Emitter ID \"$emitterIdVal\" doesn't exist or is reserved!\n" +
"BPS - Use \"/ManageEmitters list\" to view all Emitter ID's.")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(200, 0, 0).rgb)))
}
context.source.sendFeedback( { feedback }, false )
Command.SINGLE_SUCCESS
}
)
}
// list
// output -> all registered emitters
fun argList() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("list")
.executes { context ->
val allEmittersString = allEmitterIds.joinToString(", ")
val feedbackStatic = Text.literal("BPS - List of all available Emitter IDs:\n")
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)))
val feedbackVar = Text.literal(allEmittersString)
.setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(255, 255, 255).rgb)))
val feedback = feedbackStatic.append(feedbackVar)
context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS
}
}
// copy
// <emitter id>
// output -> give player a command block with the particle params (should be paste-able in kotlin)
fun argCopy() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("copy")
.then(emitterIdSuggestion
.executes { context ->
val emitterIdVal = StringArgumentType.getString(context, "Registered Emitter ID")
val commandBlock = makeCommandBlock(
emitterParamsToJson(
getEmitterParams(
emitterIdVal
)!!
)
)
val feedback: MutableText
if (context.source.server.areCommandBlocksEnabled()) {
feedback = Text.literal("BPS - Copied Emitter data as a JSON. Place down the command block and copy it to your clipboard to use the value.")
.setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb)) .setStyle(Style.EMPTY.withColor(Color(0, 200, 0).rgb))
}
else {
feedback = Text.literal("BPS - Could not copy Emitter data: Command blocks aren't enabled. Go to server.properties and enable command blocks before retrying.")
.setStyle(Style.EMPTY.withColor(Color(200, 0, 0).rgb))
Command.SINGLE_SUCCESS
}
context.source.player?.inventory?.insertStack(commandBlock)
context.source.sendFeedback({ feedback }, false) context.source.sendFeedback({ feedback }, false)
Command.SINGLE_SUCCESS Command.SINGLE_SUCCESS
} }
) )
} //.then(CommandManager.literal("copy"))
// config
// <emitter id>
// <PARTICLE / EMITTER>
// <param key> // HARDCODED KEYS, if EMITTER use emitter params.
// <new param value> // HARDCODED TYPES
// output -> update the selected parameter of the selected emitter id to the new value
fun argConfig() : LiteralArgumentBuilder<ServerCommandSource> {
return CommandManager.literal("config")
.then(emitterIdSuggestion
.then(CommandManager.literal("emitter")
.then(emitterConfigKeys.maxCount())
.then(emitterConfigKeys.spawnsPerTick())
.then(emitterConfigKeys.loopDur())
.then(emitterConfigKeys.loopDelay())
.then(emitterConfigKeys.loopCount())
) )
.then(CommandManager.literal("particle") }
.then(particleConfigKeys.shape())
.then(particleConfigKeys.blockCurve()) private fun create() : RequiredArgumentBuilder<ServerCommandSource, String> {
.then(particleConfigKeys.rotRandom()) return CommandManager.argument("New Emitter ID", StringArgumentType.string())
.then(particleConfigKeys.rotVelRandom()) .then(emitterListArg("Preset Emitter ID")
.then(particleConfigKeys.sizeRandom()) .executes { context ->
.then(particleConfigKeys.uniformSize()) val newEmitterId = StringArgumentType.getString(context, "New Emitter ID")
.then(particleConfigKeys.velRandom()) val presetEmitterId = StringArgumentType.getString(context, "Preset Emitter ID")
.then(particleConfigKeys.forceFields())
.then(particleConfigKeys.gravity()) createExec(context, newEmitterId, presetEmitterId)
.then(particleConfigKeys.drag()) Command.SINGLE_SUCCESS
.then(particleConfigKeys.minVel()) }
.then(particleConfigKeys.lifeTime())
.then(particleConfigKeys.rotVelCurve())
.then(particleConfigKeys.sizeCurve())
) )
.executes { context ->
val newEmitterId = StringArgumentType.getString(context, "New Emitter ID")
createExec(context, newEmitterId, "")
Command.SINGLE_SUCCESS
}
}
private fun createExec(context: CommandContext<ServerCommandSource>, newEmitterId: String, presetEmitterId: String) {
val params = getEmitterDataById(presetEmitterId)
val usedDefault = params == null
val result = addToRegister(
newEmitterId,
(params ?: EmitterParams.DEFAULT).deepCopy(),
) )
}
if (usedDefault && result.second) {
positiveFeedback(
fun makeCommandBlock(emitterData: Map<String, Any>): ItemStack { "Successfully added new Emitter with ID \"${result.first}\" to register.\n" +
val i = ItemStack(Items.COMMAND_BLOCK) "(DEFAULT preset was used because the specified Preset Emitter ID wasn't valid.)",
context
val nbt = NbtCompound() )
nbt.putString("Command", emitterData.toString()) }
nbt.putString("id", "minecraft:command_block") else if (!usedDefault && result.second) {
nbt.putByte("powered", 0) positiveFeedback(
nbt.putByte("auto", 0) "Successfully added new Emitter with ID \"${result.first}\" and Preset Emitter ID \"$presetEmitterId\" to register.",
nbt.putByte("UpdateLastExecution", 1) context
nbt.putByte("conditionMet", 0) )
nbt.putByte("TrackOutput", 1) }
nbt.putInt("SuccessCount", 0) else {
negativeFeedback(
"Failed to add new Emitter with ID \"${result.first}\" to register.\n" +
val component: NbtComponent = NbtComponent.of(nbt) "The provided Emitter ID is either reserved for system use or already in use.",
context
i.set(DataComponentTypes.BLOCK_ENTITY_DATA, component) )
}
return i }
private fun remove() : RequiredArgumentBuilder<ServerCommandSource, String> {
return emitterListArg("Emitter ID")
.executes { context ->
val emitterId = StringArgumentType.getString(context, "Emitter ID")
val result = removeFromRegister(emitterId)
if (result) {
positiveFeedback(
"Successfully removed Emitter ID \"$emitterId\" from register.",
context
)
}
else {
negativeFeedback(
"Failed to remove Emitter ID \"$emitterId\" from register.\nThe provided Emitter ID is either reserved for system use or already in use.",
context
)
}
Command.SINGLE_SUCCESS
}
}
private fun config(registryAccess: CommandRegistryAccess) : RequiredArgumentBuilder<ServerCommandSource, String> {
val emitterList = emitterListArg("Emitter ID")
val configArg = listConfigArgs(dataClassToArray(EmitterParams::class), emitterList, registryAccess) as RequiredArgumentBuilder<ServerCommandSource, String>
return configArg
}
// I am not going to torture myself by converting this to a json.
/*
private fun copy() : RequiredArgumentBuilder<ServerCommandSource, String> {
return emitterList("Emitter ID").executes { context ->
val emitterId = StringArgumentType.getString(context, "Emitter ID")
Command.SINGLE_SUCCESS
}
}
*/
} }
@@ -0,0 +1,61 @@
package com.bytedice.bde_particles.commands
import com.bytedice.bde_particles.*
import com.bytedice.bde_particles.particles.ParticleEmitter
import com.bytedice.bde_particles.particles.getEmitterDataById
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.command.argument.Vec3ArgumentType
import net.minecraft.server.command.CommandManager
import net.minecraft.server.command.ServerCommandSource
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
object SpawnEmitter {
fun register(dispatcher: CommandDispatcher<ServerCommandSource>) {
val command = CommandManager.literal("SpawnEmitter")
.requires { source -> source.hasPermissionLevel(2) }
dispatcher.register(
command.then(emitterListArg("Emitter ID")
.executes { context ->
execute(context, context.source.position)
Command.SINGLE_SUCCESS
}
.then(CommandManager.argument("Pos", Vec3ArgumentType.vec3())
.executes { context ->
val pos = Vec3ArgumentType.getVec3(context, "Pos")
execute(context, pos)
Command.SINGLE_SUCCESS
}
)
)
)
}
private fun execute(context: CommandContext<ServerCommandSource>, pos: Vec3d) {
val emitterId = StringArgumentType.getString(context, "Emitter ID")
val emitterParams = getEmitterDataById(emitterId)
val world = context.source.world
if (emitterParams != null) {
val emitter = ParticleEmitter(
pos,
Vector2f(0.0f, 0.0f),
world,
emitterParams,
world.gameRules.getBoolean(Bde_particles.SHOW_PARTICLE_DEBUG)
)
spawnEmitter(emitter)
positiveFeedback("Successfully spawned Emitter ID \"$emitterId\"", context)
}
else {
negativeFeedback("Failed to spawn Emitter ID \"$emitterId\". Emitter ID doesn't exist!", context)
}
}
}
@@ -0,0 +1,167 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.Billboard
import com.bytedice.bde_particles.DisplayEntity
import com.bytedice.bde_particles.DisplayEntityProperties
import com.bytedice.bde_particles.SESSION_UUID
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
import org.joml.Vector3f
import org.joml.Vector4f
class EmitterDebug(
rot: Vector2f,
eOrigin: Vec3d,
spawnPosOffset: Vector3f,
shape: SpawningShape,
) {
private val eOriginDebug = EOrigin(rot, eOrigin)
private val shapeDebug = ShapeDebug(eOrigin, spawnPosOffset, shape)
private val forceFieldDebug = ForceFieldDebug()
fun spawn(world: ServerWorld, eOrigin: Vec3d, forceFields: ParamClasses.ForceFieldArray, spawnPosOffset: Vector3f) {
eOriginDebug.spawn(world)
shapeDebug.spawn(world)
forceFieldDebug.makeArray(eOrigin, forceFields, spawnPosOffset)
forceFieldDebug.spawn(world)
}
fun update(
rot: Vector2f,
eOrigin: Vec3d,
spawnPosOffset: Vector3f
) {
eOriginDebug.update(rot, eOrigin)
shapeDebug.update(rot, eOrigin, spawnPosOffset)
}
fun kill() {
eOriginDebug.kill()
shapeDebug.kill()
forceFieldDebug.kill()
}
}
class EOrigin(rot: Vector2f, eOrigin: Vec3d) {
val eOriginDE = DisplayEntity(
DisplayEntityProperties(
pos = eOrigin,
rot = rot,
model = "minecraft:red_concrete",
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
scale = Vector3f(0.25f, 0.25f, 0.25f)
))
fun spawn(world: ServerWorld) {
eOriginDE.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d) {
val newProperties = eOriginDE.properties
newProperties?.rot = rot
newProperties?.pos = eOrigin
eOriginDE.updateProperties(newProperties!!)
}
fun kill() {
eOriginDE.kill()
}
}
class ShapeDebug(eOrigin: Vec3d, spawnPosOffset: Vector3f, shape: SpawningShape) {
val shapeScale: Vector3f? = when (shape) {
is SpawningShape.Circle -> Vector3f(shape.radius * 2 * 2, shape.radius * 2 * 2, 0.0001f)
is SpawningShape.Sphere -> Vector3f(shape.radius * 2 * 2, shape.radius * 2 * 2, 0.0001f)
is SpawningShape.Rect -> Vector3f(shape.size.x, 0.0001f, shape.size.y)
is SpawningShape.Cube -> Vector3f(shape.size.x, shape.size.y, shape.size.z)
else -> null
}
val shapeRot = when (shape) {
is SpawningShape.Circle -> Vector4f(-0.707f, 0.0f, 0.0f, 0.707f)
else -> Vector4f(0.0f, 0.0f, 0.0f, 1.0f)
}
private fun determineModel(shape: SpawningShape): String =
when (shape) {
is SpawningShape.Circle, is SpawningShape.Sphere -> "minecraft:sunflower"
else -> "minecraft:yellow_stained_glass"
}
private fun determineBillboard(shape: SpawningShape): String =
if (shape !is SpawningShape.Sphere) Billboard.FIXED else Billboard.CENTER
val shapeDE: DisplayEntity? = if (shape !is SpawningShape.Point) {
val position = eOrigin.add(
spawnPosOffset.x.toDouble(),
spawnPosOffset.y.toDouble(),
spawnPosOffset.z.toDouble()
)
DisplayEntity(DisplayEntityProperties(
pos = position.add(spawnPosOffset.x.toDouble(), spawnPosOffset.y.toDouble(), spawnPosOffset.z.toDouble()),
model = determineModel(shape),
tags = arrayOf("DEBUG", "P_DEBUG", "shape_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
billboard = determineBillboard(shape),
leftRotation = shapeRot,
scale = shapeScale ?: Vector3f(1f, 1f, 1f)
))
} else null
fun spawn(world: ServerWorld) {
shapeDE?.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d, spawnPosOffset: Vector3f) {
val newProperties = shapeDE?.properties
newProperties?.rot = rot
newProperties?.pos = eOrigin
newProperties?.translation = spawnPosOffset
shapeDE?.updateProperties(newProperties!!)
}
fun kill() {
shapeDE?.kill()
}
}
class ForceFieldDebug {
var FFDEArray: Array<DisplayEntity> = emptyArray()
fun makeArray(eOrigin: Vec3d, forceFieldArray: ParamClasses.ForceFieldArray, spawnPosOffset: Vector3f) {
forceFieldArray.array.forEachIndexed { index, it ->
val shapeScale = when (it.shape) {
is ForceFieldShape.Sphere -> Vector3f(1.333f * it.shape.radius * 2, 1.333f * it.shape.radius * 2, 0.0001f)
is ForceFieldShape.Cube -> it.shape.size
}
val newOffset = Vector3f(spawnPosOffset).add(it.pos)
val displayEntity = DisplayEntity(DisplayEntityProperties(
pos = eOrigin.add(newOffset.x.toDouble(), newOffset.y.toDouble(), newOffset.z.toDouble()),
model = if (it.shape is ForceFieldShape.Sphere) { "minecraft:snowball" } else { "minecraft:white_stained_glass" },
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
customName = "forceField index: $index",
brightnessOverride = 15,
billboard = if (it.shape is ForceFieldShape.Sphere) { Billboard.CENTER } else { Billboard.FIXED },
scale = shapeScale
))
FFDEArray += displayEntity
}
}
fun spawn(world: ServerWorld) {
FFDEArray.forEach { it.spawn(world) }
}
fun kill() {
FFDEArray.forEach { it.kill() }
}
}
@@ -1,42 +1,159 @@
package com.bytedice.bde_particles.particles package com.bytedice.bde_particles.particles
/** import com.bytedice.bde_particles.LerpCurves
* EmitterParams defines the parameters for controlling how particles are emitted, import org.joml.Vector3f
* including settings for looping, spawn frequency, and maximum particle count.
*
* @param maxCount The maximum number of particles that can exist. If set to 0 or below, no particles will spawn.
* @param spawnsPerTick The number of particles spawned per tick. If set to 0 or below, no particles will spawn.
* @param loopDur The duration for which the loop runs, in ticks. A value 0 or below means the loop is infinite, and `loopDelay` and `loopCount` will be ignored.
* @param loopDelay The delay between each loop cycle, in ticks. Negative values are treated as 0, meaning no delay.
* @param loopCount The number of times the loop will repeat. 0 means it will shoot a single burst of particles. A value below 0 means it will repeat indefinitely.
* @param particle The particle params of the particle that will spawn.
*/
data class EmitterParams ( data class EmitterParams (
var maxCount: Int = 200, // spawning
var spawnsPerTick: Int = 1, var maxCount: Int,
var loopDur: Int = 25, var spawnRate: Int,
var loopDelay: Int = 0, var spawnChance: Float,
var loopCount: Int = 0, var spawnDuration: ParamClasses.Duration,
var particle: ParticleParams = ParticleParams.DEFAULT, var spawnPosOffset: Vector3f,
var lifeTime: ParamClasses.PairInt,
var shape: SpawningShape,
// init transforms
var offset: ParamClasses.PairVec3f,
var initRot: ParamClasses.PairVec3f,
var rotVel: ParamClasses.PairVec3f,
var initVel: ParamClasses.PairVec3f,
var initCenterVel: ParamClasses.PairFloat,
var initScale: ParamClasses.PairVec3f,
var transformWithVel: ParamClasses.TransformWithVel,
// velocity
var forceFields: ParamClasses.ForceFieldArray,
var constVel: Vector3f,
var drag: Float,
var minVel: Float,
// curves
var offsetCurve: ParamClasses.LerpValVec3f,
var rotVelCurve: ParamClasses.LerpValVec3f,
var scaleCurve: ParamClasses.LerpValVec3f,
var modelCurve: ParamClasses.StringCurve,
var brightnessCurve: ParamClasses.LerpValInt,
) )
{ {
fun deepCopy() : EmitterParams {
val cModelCurveArray = this.modelCurve.deepCopy()
val cForceFieldArray = this.forceFields.deepCopy()
val cParams = this.copy()
cParams.modelCurve = cModelCurveArray
cParams.forceFields = cForceFieldArray
return cParams
}
companion object Presets { companion object Presets {
val DEFAULT = EmitterParams() val DEFAULT = EmitterParams(
val FIRE_GEYSER = EmitterParams( maxCount = 200,
200, spawnRate = 50,
1, spawnChance = 1.0f,
25, spawnDuration = ParamClasses.Duration.SingleBurst(4),
0, spawnPosOffset = Vector3f(0.0f, 0.0f, 0.0f),
0, lifeTime = ParamClasses.PairInt(25, 40),
ParticleParams.FIRE_GEYSER shape = SpawningShape.Sphere(1.0f, true),
offset = ParamClasses.PairVec3f.Uniform(0.0f, 0.0f),
initRot = ParamClasses.PairVec3f.NonUniform(0.0f, 0.0f, 0.0f, 360.0f, 360.0f, 360.0f),
rotVel = ParamClasses.PairVec3f.NonUniform(-0.2f, -0.2f, -0.2f, 0.2f, 0.2f, 0.2f),
initVel = ParamClasses.PairVec3f.Null,
initCenterVel = ParamClasses.PairFloat(0.4f, 0.5f),
initScale = ParamClasses.PairVec3f.Uniform(0.5f, 1.5f),
transformWithVel = ParamClasses.TransformWithVel.Null,
forceFields = ParamClasses.ForceFieldArray(emptyArray()),
constVel = Vector3f(0.0f, 0.0f, 0.0f),
drag = 0.075f,
minVel = 0.0f,
offsetCurve = ParamClasses.LerpValVec3f.Null,
rotVelCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 0.0f, LerpCurves.Sqrt),
scaleCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 2.5f, LerpCurves.Linear),
modelCurve = ParamClasses.StringCurve(
arrayOf(
"shroomlight",
"orange_concrete",
"orange_stained_glass",
"gray_wool",
"gray_stained_glass",
"light_gray_stained_glass"
),
LerpCurves.Sqrt
),
brightnessCurve = ParamClasses.LerpValInt.LerpInt(15, 5, LerpCurves.Sqrt)
) )
val RING_EXPLOSION = EmitterParams( val DEBUG = EmitterParams(
200, maxCount = 50,
150, spawnRate = 10,
1, spawnChance = 1.0f,
0, spawnDuration = ParamClasses.Duration.SingleBurst(5),
0, spawnPosOffset = Vector3f(0.0f, 2.0f, 0.0f),
ParticleParams.RING_EXPLOSION lifeTime = ParamClasses.PairInt(25, 40),
shape = SpawningShape.Circle(1.0f, true),
offset = ParamClasses.PairVec3f.Uniform(0.0f, 0.0f),
initRot = ParamClasses.PairVec3f.Null,
rotVel = ParamClasses.PairVec3f.Null,
initVel = ParamClasses.PairVec3f.NonUniform(0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f),
initCenterVel = ParamClasses.PairFloat(1.5f, 1.5f),
initScale = ParamClasses.PairVec3f.Uniform(2.0f, 2.0f),
transformWithVel = ParamClasses.TransformWithVel.ScaleAndRot(0.75f),
forceFields = ParamClasses.ForceFieldArray(emptyArray()),
constVel = Vector3f(0.0f, -0.05f, 0.0f),
drag = 0.075f,
minVel = 0.0f,
offsetCurve = ParamClasses.LerpValVec3f.Null,
rotVelCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 0.0f, LerpCurves.Linear),
scaleCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 1.0f, LerpCurves.Linear),
modelCurve = ParamClasses.StringCurve(
arrayOf(
"shroomlight",
"orange_concrete",
"orange_stained_glass",
"gray_wool",
"gray_stained_glass",
"light_gray_stained_glass"
),
LerpCurves.Linear
),
brightnessCurve = ParamClasses.LerpValInt.LerpInt(15, 5, LerpCurves.Sqrt)
)
val STRESS_TEST = EmitterParams(
maxCount = 10000,
spawnRate = 50,
spawnChance = 1.0f,
spawnDuration = ParamClasses.Duration.SingleBurst(100),
spawnPosOffset = Vector3f(0.0f, 0.0f, 0.0f),
lifeTime = ParamClasses.PairInt(75, 100),
shape = SpawningShape.Circle(10.0f, true),
offset = ParamClasses.PairVec3f.Uniform(0.0f, 0.0f),
initRot = ParamClasses.PairVec3f.NonUniform(0.0f, 0.0f, 0.0f, 360.0f, 360.0f, 360.0f),
rotVel = ParamClasses.PairVec3f.NonUniform(-0.2f, -0.2f, -0.2f, 0.2f, 0.2f, 0.2f),
initVel = ParamClasses.PairVec3f.NonUniform(0.0f, 4.0f, 0.0f, 0.0f, 8.0f, 0.0f),
initCenterVel = ParamClasses.PairFloat(-1.5f, -1.5f),
initScale = ParamClasses.PairVec3f.Uniform(10.0f, 15.0f),
transformWithVel = ParamClasses.TransformWithVel.Null,
forceFields = ParamClasses.ForceFieldArray(
arrayOf(ForceField(
Vector3f(0.0f, 75.0f, 0.0f),
ForceFieldShape.Sphere(15.0f, Pair(0.1f, 0.2f))
))
),
constVel = Vector3f(0.0f, 0.00001f, 0.0f),
drag = 0.075f,
minVel = 0.0f,
offsetCurve = ParamClasses.LerpValVec3f.Null,
rotVelCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 0.0f, LerpCurves.Linear),
scaleCurve = ParamClasses.LerpValVec3f.LerpUniform(1.0f, 1.5f, LerpCurves.Linear),
modelCurve = ParamClasses.StringCurve(
arrayOf(
"shroomlight",
"orange_concrete",
"orange_stained_glass",
"gray_wool",
"gray_stained_glass",
"light_gray_stained_glass"
),
LerpCurves.Linear
),
brightnessCurve = ParamClasses.LerpValInt.Null
) )
} }
} }
@@ -3,7 +3,13 @@ package com.bytedice.bde_particles.particles
import org.joml.Vector3f import org.joml.Vector3f
data class ForceField ( data class ForceField (
val name: String = "DEFAULT", val pos: Vector3f,
val pos: Vector3f = Vector3f(0.0f, 5.0f, 0.0f), val shape: ForceFieldShape
val shape: ForceFieldShape = ForceFieldShape.SPHERE() ) {
) fun deepCopy(): ForceField {
return ForceField(
Vector3f(pos),
shape.deepCopy()
)
}
}
@@ -0,0 +1,208 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.LerpCurves
import com.bytedice.bde_particles.lerpArray
import com.bytedice.bde_particles.randomFloatBetween
import com.bytedice.bde_particles.randomIntBetween
import org.joml.Vector3f
import kotlin.math.atan2
import kotlin.math.pow
import kotlin.math.sqrt
class ParamClasses {
sealed class PairVec3f {
class Uniform (
private val min: Float,
private val max: Float,
) : PairVec3f() {
fun randomize() : Vector3f {
val x = randomFloatBetween(min, max)
return Vector3f(x, x, x)
}
}
class NonUniform (
private val minX: Float,
private val minY: Float,
private val minZ: Float,
private val maxX: Float,
private val maxY: Float,
private val maxZ: Float
) : PairVec3f() {
fun randomize(): Vector3f {
return Vector3f(
randomFloatBetween(minX, maxX),
randomFloatBetween(minY, maxY),
randomFloatBetween(minZ, maxZ),
)
}
}
data class Constant(val value: Vector3f) : PairVec3f()
data object Null : PairVec3f()
}
class PairInt (
val min: Int,
val max: Int,
) {
fun randomize() : Int { return randomIntBetween(min, max) }
}
class PairFloat(
val min: Float,
val max: Float,
) {
fun randomize() : Float { return randomFloatBetween(min, max) }
}
sealed class LerpValVec3f {
class LerpVec3f(
private val fromX: Float,
private val fromY: Float,
private val fromZ: Float,
private val toX: Float,
private val toY: Float,
private val toZ: Float,
private val curve: LerpCurves,
) : LerpValVec3f() {
fun lerp(t: Float): Vector3f {
val x = com.bytedice.bde_particles.lerp(fromX, toX, curve.function(t))
val y = com.bytedice.bde_particles.lerp(fromY, toY, curve.function(t))
val z = com.bytedice.bde_particles.lerp(fromZ, toZ, curve.function(t))
return Vector3f(x, y, z)
}
}
class MultiLerpVec3f(
private val fromX: Float,
private val fromY: Float,
private val fromZ: Float,
private val toX: Float,
private val toY: Float,
private val toZ: Float,
private val curveX: LerpCurves,
private val curveY: LerpCurves,
private val curveZ: LerpCurves
) : LerpValVec3f() {
fun lerp(t: Float): Vector3f {
val x = com.bytedice.bde_particles.lerp(fromX, toX, curveX.function(t))
val y = com.bytedice.bde_particles.lerp(fromY, toY, curveY.function(t))
val z = com.bytedice.bde_particles.lerp(fromZ, toZ, curveZ.function(t))
return Vector3f(x, y, z)
}
}
class LerpUniform(
private val from: Float,
private val to: Float,
private val curve: LerpCurves,
) : LerpValVec3f() {
fun lerp(t: Float) : Float {
return com.bytedice.bde_particles.lerp(from, to, curve.function(t))
}
}
data class Constant(val value: Vector3f) : LerpValVec3f()
data object Null : LerpValVec3f()
fun lerpToVector3f(t: Float) : Vector3f {
when (this) {
is LerpVec3f -> return this.lerp(t)
is MultiLerpVec3f -> return this.lerp(t)
is LerpUniform -> {
val x = this.lerp(t)
return Vector3f(x, x, x)
}
is Constant -> return this.value
is Null -> return Vector3f(1.0f, 1.0f, 1.0f)
}
}
}
sealed class LerpValInt {
class LerpInt(
private val from: Int,
private val to: Int,
private val curve: LerpCurves,
) : LerpValInt() {
fun lerp(t: Float): Int {
return Math.round(com.bytedice.bde_particles.lerp(from.toFloat(), to.toFloat(), curve.function(t)))
}
}
data class Constant(val value: Int) : LerpValInt()
data object Null: LerpValInt()
fun lerpToInt(t: Float) : Int? {
return when (this) {
is LerpInt -> this.lerp(t)
is Constant -> this.value
is Null -> null
}
}
}
sealed class Duration {
data class SingleBurst(
val loopDur: Int
) : Duration()
data class MultiBurst(
val loopDur: Int,
val loopDelay: Int,
val loopCount: Int,
) : Duration()
data class InfiniteLoop(
val loopDur: Int,
val loopDelay: Int
) : Duration()
}
sealed class TransformWithVel {
data object RotOnly : TransformWithVel() {
fun velToRot(velocity: Vector3f): Vector3f {
val normalizedVelocity = Vector3f(velocity).normalize()
val yaw = Math.toDegrees(atan2(normalizedVelocity.x.toDouble(), normalizedVelocity.z.toDouble())).toFloat()
val horizontalMag = sqrt(normalizedVelocity.x.toDouble().pow(2) + normalizedVelocity.z.toDouble().pow(2))
val pitch = Math.toDegrees(atan2(-normalizedVelocity.y.toDouble(), horizontalMag)).toFloat()
return Vector3f(pitch, yaw, 0f)
}
}
class ScaleAndRot(private val scaleMul: Float) : TransformWithVel() {
fun velToRot(velocity: Vector3f) : Vector3f {
val normalizedVelocity = Vector3f(velocity).normalize()
val yaw = Math.toDegrees(atan2(normalizedVelocity.x.toDouble(), normalizedVelocity.z.toDouble())).toFloat()
val horizontalMag = sqrt(normalizedVelocity.x.toDouble().pow(2) + normalizedVelocity.z.toDouble().pow(2))
val pitch = Math.toDegrees(atan2(-normalizedVelocity.y.toDouble(), horizontalMag)).toFloat()
return Vector3f(pitch, yaw, 0f)
}
fun velToScale(velocity: Vector3f): Vector3f {
val velocityMagnitude = velocity.length()
val stretchFactor = velocityMagnitude * scaleMul + 1.0f
val contractFactor = 1.0f / stretchFactor
return Vector3f(contractFactor, contractFactor, stretchFactor)
}
}
data object Null : TransformWithVel()
}
class StringCurve (
var array: Array<String>,
var curve: LerpCurves
) {
fun lerp(t: Float) : String { return lerpArray(array as Array<Any>, t, curve) as String }
fun deepCopy(): StringCurve {
return StringCurve(array.copyOf(), curve.deepCopy())
}
}
data class ForceFieldArray (
var array: Array<ForceField>
) {
fun deepCopy(): ForceFieldArray {
return ForceFieldArray(array.map { it.deepCopy() }.toTypedArray())
}
}
}
@@ -1,77 +1,110 @@
package com.bytedice.bde_particles.particles package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.* import com.bytedice.bde_particles.*
import net.minecraft.particle.DustParticleEffect
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
import org.joml.Vector3f import org.joml.Vector3f
import org.joml.Vector4f import org.joml.Vector4f
import kotlin.math.* import kotlin.math.*
// TODO: consider moving most params to emitter (such as shape, and forceFields) to store less data class Particle(
private val emitterParams: EmitterParams,
private var entityPos: Vec3d,
class Particle(private val particleParams: ParticleParams) { private var entityRot: Vector2f,
private val initOffset = Vector3f(-0.5f, -0.5f, -0.5f) private val debug: Boolean = false
private var offset = Vector3f(0.0f, 0.0f, 0.0f) ) {
private var pos = Vec3d(0.0, 0.0, 0.0) private var pos = Vector3f(0.0f, 0.0f, 0.0f)
private var rot = randomBetweenVector3f(particleParams.rotRandom.first, particleParams.rotRandom.second)
private val rotVel = randomBetweenVector3f(particleParams.rotVelRandom.first, particleParams.rotVelRandom.second)
private var vel = randomBetweenVector3f(particleParams.velRandom.first, particleParams.velRandom.second)
private var scale = randomBetweenVector3f(particleParams.sizeRandom.first, particleParams.sizeRandom.second)
private var blockDisplay: DisplayEntity? = null
private var lifeTime = randomIntBetween(particleParams.lifeTime.first, particleParams.lifeTime.second)
var isDead = false
private var timeAlive = 0 private var timeAlive = 0
private var isInit = false private var isInit = false
var isDead = false
// params
fun init(pos: Vec3d) { private val originOffset = when (val offset = emitterParams.offset) {
this.pos = pos is ParamClasses.PairVec3f.Uniform -> offset.randomize()
is ParamClasses.PairVec3f.NonUniform -> offset.randomize()
if ( particleParams.uniformSize ) { is ParamClasses.PairVec3f.Constant -> offset.value
val randomUniform = randomFloatBetween(particleParams.sizeRandom.first.x, particleParams.sizeRandom.second.x) is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
scale.x = randomUniform
scale.y = randomUniform
scale.z = randomUniform
} }
val shape = particleParams.shape private var rot = when (val initRot = emitterParams.initRot) {
val newSpawnPos: Vector3f is ParamClasses.PairVec3f.Uniform -> initRot.randomize()
is ParamClasses.PairVec3f.NonUniform -> initRot.randomize()
when (shape) { is ParamClasses.PairVec3f.Constant -> initRot.value
is SpawningShape.CIRCLE -> { val posInCircle = randomInCircle(shape.radius); newSpawnPos = Vector3f(posInCircle.x, 0.0f, posInCircle.y) } is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
is SpawningShape.SPHERE -> { newSpawnPos = randomInSphere(shape.radius) }
is SpawningShape.RECT -> { val posInRect = randomInRect(shape.size); newSpawnPos = Vector3f(posInRect.x, 0.0f, posInRect.y) }
is SpawningShape.CUBE -> { newSpawnPos = randomInCube(shape.size) }
is SpawningShape.POINT -> { newSpawnPos = Vector3f(0.0f, 0.0f, 0.0f) }
} }
offset = newSpawnPos private var rotVel = when (val initRotVel = emitterParams.rotVel) {
is ParamClasses.PairVec3f.Uniform -> initRotVel.randomize()
is ParamClasses.PairVec3f.NonUniform -> initRotVel.randomize()
is ParamClasses.PairVec3f.Constant -> initRotVel.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
val (quatRot, newOffset) = calcTransformOffset(rot, initOffset, scale) private var vel = when (val initVel = emitterParams.initVel) {
is ParamClasses.PairVec3f.Uniform -> initVel.randomize()
is ParamClasses.PairVec3f.NonUniform -> initVel.randomize()
is ParamClasses.PairVec3f.Constant -> initVel.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
private var scale = when (val initScale = emitterParams.initScale) {
is ParamClasses.PairVec3f.Uniform -> initScale.randomize()
is ParamClasses.PairVec3f.NonUniform -> initScale.randomize()
is ParamClasses.PairVec3f.Constant -> initScale.value
is ParamClasses.PairVec3f.Null -> Vector3f(0.0f, 0.0f, 0.0f)
}
private var lifeTime = emitterParams.lifeTime.randomize()
private lateinit var particleEntity: DisplayEntity
private val debugTool = ParticleDebug(entityRot, entityPos, pos, vel, scale)
fun init() {
val so = Vector3f(emitterParams.spawnPosOffset)
if (so.x + so.y + so.z != 0.0f) {
entityPos = entityPos.add(so.x.toDouble(), so.y.toDouble(), so.z.toDouble())
}
val shape = emitterParams.shape
val newSpawnPos = shape.random(shape)
pos = newSpawnPos
val posDouble = Vec3d(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble())
val dirFromCenter = posDouble.normalize()
val forceFromCenter = dirFromCenter.multiply(emitterParams.initCenterVel.randomize().toDouble())
vel.add(forceFromCenter.x.toFloat(), forceFromCenter.y.toFloat(), forceFromCenter.z.toFloat())
val (quatRot, newPos) = calcTransformOffset(this.rot, originOffset, scale)
val properties = DisplayEntityProperties( val properties = DisplayEntityProperties(
pos = this.pos, pos = entityPos,
blockType = particleParams.blockCurve[0], rot = entityRot,
translation = newOffset.add(offset), model = if (emitterParams.modelCurve.array.isNotEmpty()) { emitterParams.modelCurve.lerp(0.0f) } else "none",
translation = newPos.add(pos),
leftRotation = quatRot, leftRotation = quatRot,
scale = scale, scale = Vector3f(scale).mul(emitterParams.scaleCurve.lerpToVector3f(0.0f)),
tags = arrayOf("BPS_Particle", "BPS_UUID", sessionUuid.toString()) tags = arrayOf("BPS_Particle", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = emitterParams.brightnessCurve.lerpToInt(0.0f)
) )
blockDisplay = DisplayEntity(properties) particleEntity = DisplayEntity(properties)
isInit = true isInit = true
} }
fun spawn(world: ServerWorld) { fun spawn(world: ServerWorld) {
if (isInit) { blockDisplay?.spawn(world) } if (isInit) {
particleEntity.spawn(world)
if (debug) { debugTool.spawn(world) }
LIVING_PARTICLE_COUNT += 1
}
else { error("Particle is not initialized before being spawned. Please use Particle.init() to initialize it.") } else { error("Particle is not initialized before being spawned. Please use Particle.init() to initialize it.") }
} }
@@ -81,9 +114,9 @@ class Particle(private val particleParams: ParticleParams) {
val newProperties = calcNewProperties() val newProperties = calcNewProperties()
blockDisplay?.updateProperties(newProperties) particleEntity.updateProperties(newProperties)
if (timeAlive > lifeTime) { isDead = true; blockDisplay?.kill() } if (timeAlive > lifeTime) { kill() }
timeAlive += 1 timeAlive += 1
} }
@@ -103,100 +136,130 @@ class Particle(private val particleParams: ParticleParams) {
private fun calcNewProperties() : DisplayEntityProperties { private fun calcNewProperties() : DisplayEntityProperties {
val timeAliveClamped = (timeAlive.toFloat() / lifeTime.toFloat()).coerceIn(0.0f, 1.0f)
fun calcBlockCurve(timeAliveClamped: Float) : String { val newModel = if (emitterParams.modelCurve.array.isNotEmpty()) { emitterParams.modelCurve.lerp(timeAliveClamped) } else "none"
val newBlockIdx = round(lerp(0.0f, particleParams.blockCurve.lastIndex.toFloat(), timeAliveClamped)).toInt() val (newVel, newPos) = calcPos()
return particleParams.blockCurve[newBlockIdx] val newOriginOffset = Vector3f(originOffset.mul(emitterParams.offsetCurve.lerpToVector3f(timeAliveClamped)))
val newBrightness = emitterParams.brightnessCurve.lerpToInt(timeAliveClamped)
val TWVScale: Vector3f
//val TWVRot: Vector3f
val newRot: Vector3f
when (emitterParams.transformWithVel) {
is ParamClasses.TransformWithVel.RotOnly -> {
TWVScale = Vector3f(1.0f, 1.0f, 1.0f)
newRot = (emitterParams.transformWithVel as ParamClasses.TransformWithVel.RotOnly).velToRot(vel)
}
is ParamClasses.TransformWithVel.ScaleAndRot -> {
TWVScale = (emitterParams.transformWithVel as ParamClasses.TransformWithVel.ScaleAndRot).velToScale(vel)
newRot = (emitterParams.transformWithVel as ParamClasses.TransformWithVel.ScaleAndRot).velToRot(vel)
}
else -> {
TWVScale = Vector3f(1.0f, 1.0f, 1.0f)
newRot = calcRot(timeAliveClamped)
}
} }
fun sphereSdfVel(forceField: ForceField) : Vector3f { val newScale = TWVScale.mul(calcScale(timeAliveClamped))
if (forceField.shape !is ForceFieldShape.SPHERE) { return Vector3f(0.0f, 0.0f, 0.0f) } //val newRot = TWVRot.add(calcRot(timeAliveClamped))
rot = newRot
vel = newVel
pos = newPos
for (forceField in emitterParams.forceFields.array) {
if (forceField.shape is ForceFieldShape.Sphere) { vel.add(sphereSdfVel(forceField)) }
else if (forceField.shape is ForceFieldShape.Cube) { vel.add(cubeSdfVel(forceField)) }
}
val (quatRot, transformOffset) = calcTransformOffset(newRot, originOffset, newScale)
val combinedOffset = transformOffset.add(newPos)
val newTranslate = combinedOffset.add(newOriginOffset)
val newProperties = DisplayEntityProperties(
pos = entityPos,
rot = entityRot,
model = newModel,
translation = newTranslate,
leftRotation = quatRot,
scale = newScale,
tags = arrayOf("BPS_Particle", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = newBrightness
)
if (debug) { debugTool.update(entityRot, entityPos, newTranslate, vel, newScale) }
return newProperties
}
private fun sphereSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.Sphere) { return Vector3f(0.0f, 0.0f, 0.0f) }
val shape = forceField.shape val shape = forceField.shape
val sdfVal = sdfSphere(forceField.pos, shape.radius, offset) val sdfVal = sdfSphere(forceField.pos, shape.radius, pos)
val normalizedSdf = normalizeSdf(sdfVal, shape.radius) val normalizedSdf = normalizeSdf(sdfVal, shape.radius)
val velDir = Vector3f(offset).sub(forceField.pos).normalize() val velDir = Vector3f(pos).sub(forceField.pos).normalize()
val velMul = if (sdfVal > 0) { 0.0f } val velMul = if (sdfVal > 0) { 0.0f }
else { lerp(shape.force.second, shape.force.second, 1 - normalizedSdf) } else { lerp(shape.force.second, shape.force.second, 1 - normalizedSdf) }
return velDir.mul(velMul) return velDir.mul(velMul)
} }
fun cubeSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.CUBE) { return Vector3f(0.0f, 0.0f, 0.0f) } private fun cubeSdfVel(forceField: ForceField) : Vector3f {
if (forceField.shape !is ForceFieldShape.Cube) { return Vector3f(0.0f, 0.0f, 0.0f) }
val shape = forceField.shape val shape = forceField.shape
val sdfVal = sdfCube(forceField.pos, shape.size, offset) val sdfVal = sdfCube(forceField.pos, shape.size, pos)
val velDir = if (sdfVal < 0) { shape.forceDir } val velDir = if (sdfVal < 0) { shape.force }
else { Vector3f(0.0f, 0.0f, 0.0f) } else { Vector3f(0.0f, 0.0f, 0.0f) }
return velDir return velDir
} }
fun calcRot(timeAliveClamped: Float) : Vector3f {
private fun calcRot(t: Float) : Vector3f {
val newRot = Vector3f(rot) val newRot = Vector3f(rot)
val newVel = Vector3f(rotVel) val newVel = Vector3f(rotVel)
val rotCurve = particleParams.rotVelCurve
newVel.mul(interpolateCurve(rotCurve.first, rotCurve.second, timeAliveClamped, rotCurve.third)) newVel.mul(emitterParams.rotVelCurve.lerpToVector3f(t))
newRot.add(newVel) return newRot.add(newVel)
return newRot
} }
fun calcOffset() : Pair<Vector3f, Vector3f> {
private fun calcPos() : Pair<Vector3f, Vector3f> {
var vel = Vector3f(vel) var vel = Vector3f(vel)
vel = vel.add(particleParams.gravity) val constVel = emitterParams.constVel
vel = vel.mul(1.0f - particleParams.drag) if (constVel.x + constVel.y + constVel.z != 0.0f) {
vel = vel.add(emitterParams.constVel)
}
vel = vel.mul(1.0f - emitterParams.drag)
if (abs(vel.x) < particleParams.minVel) { vel.x = 0.0f } if (abs(vel.x) < emitterParams.minVel) { vel.x = 0.0f }
if (abs(vel.y) < particleParams.minVel) { vel.y = 0.0f } if (abs(vel.y) < emitterParams.minVel) { vel.y = 0.0f }
if (abs(vel.z) < particleParams.minVel) { vel.z = 0.0f } if (abs(vel.z) < emitterParams.minVel) { vel.z = 0.0f }
var newOffset = Vector3f(offset) var newPos = Vector3f(pos)
newOffset = newOffset.add(vel) newPos = newPos.add(vel)
return Pair(vel, newOffset) return Pair(vel, newPos)
} }
fun calcSize(timeAliveClamped: Float) : Vector3f {
val scale = Vector3f(scale)
val sizeCurve = particleParams.sizeCurve
scale.mul(interpolateCurve(sizeCurve.first, sizeCurve.second, timeAliveClamped, sizeCurve.third))
return scale
}
val timeAliveClamped = (timeAlive.toFloat() / lifeTime.toFloat()).coerceIn(0.0f, 1.0f) private fun calcScale(t: Float) : Vector3f {
val newScale = Vector3f(scale)
val newBlock = calcBlockCurve(timeAliveClamped) return newScale.mul(emitterParams.scaleCurve.lerpToVector3f(t))
val (newVel, newOffset) = calcOffset()
val newRot = calcRot(timeAliveClamped)
val newScale = calcSize(timeAliveClamped)
vel = newVel
offset = newOffset
rot = newRot
for (forceField in particleParams.forceFields) {
if (forceField.shape is ForceFieldShape.SPHERE) { vel.add(sphereSdfVel(forceField)) }
else if (forceField.shape is ForceFieldShape.CUBE) { vel.add(cubeSdfVel(forceField)) }
}
val (quatRot, transformOffset) = calcTransformOffset(newRot, initOffset, newScale)
val combinedOffset = transformOffset.add(newOffset)
val newProperties = DisplayEntityProperties(
pos = pos,
blockType = newBlock,
translation = combinedOffset,
leftRotation = quatRot,
scale = newScale,
tags = arrayOf("BPS_Particle", "BPS_UUID", sessionUuid.toString())
)
return newProperties
} }
fun kill() { fun kill() {
blockDisplay?.kill() if (!isDead) {
particleEntity.kill()
if (debug) { debugTool.kill() }
LIVING_PARTICLE_COUNT -= 1
isDead = true
}
} }
} }
@@ -0,0 +1,120 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.*
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
import org.joml.Vector3f
import kotlin.math.atan2
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sqrt
class ParticleDebug(
rot: Vector2f,
eOrigin: Vec3d,
pOrigin: Vector3f,
pVel: Vector3f,
pScale: Vector3f
) {
private val pOriginDebug = POrigin(rot, eOrigin, pOrigin)
private val pVelDebug = PVel(rot, eOrigin, pOrigin, pVel, pScale)
fun spawn(world: ServerWorld) {
pOriginDebug.spawn(world)
pVelDebug.spawn(world)
}
fun update(
rot: Vector2f,
eOrigin: Vec3d,
pOrigin: Vector3f,
pVel: Vector3f,
pScale: Vector3f
) {
pOriginDebug.update(rot, eOrigin, pOrigin)
pVelDebug.update(rot, eOrigin, pOrigin, pVel, pScale)
}
fun kill() {
pOriginDebug.kill()
pVelDebug.kill()
}
}
class POrigin(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f) {
val pOriginDE = DisplayEntity(DisplayEntityProperties(
pos = eOrigin,
rot = rot,
model = "minecraft:lime_concrete",
tags = arrayOf("DEBUG", "P_DEBUG", "pOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
translation = Vector3f(pOrigin),
scale = Vector3f(0.15f, 0.15f, 0.15f)
))
fun spawn(world: ServerWorld) {
pOriginDE.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f) {
val newProperties = pOriginDE.properties
newProperties?.rot = rot
newProperties?.pos = eOrigin
newProperties?.translation = Vector3f(pOrigin)
pOriginDE.updateProperties(newProperties!!)
}
fun kill() {
pOriginDE.kill()
}
}
class PVel(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f, pVel: Vector3f, pScale: Vector3f) {
val pVelDE = DisplayEntity(DisplayEntityProperties(
pos = eOrigin,
rot = rot,
model = "minecraft:red_nether_brick_slab", //"minecraft:red_stained_glass",
tags = arrayOf("DEBUG", "P_DEBUG", "pVel_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
brightnessOverride = 15,
translation = Vector3f(pOrigin),
scale = Vector3f(0.05f, max(max(pScale.x, pScale.y), pScale.z) + (pVel.x + pVel.y + pVel.z), 0.05f)
))
fun spawn(world: ServerWorld) {
pVelDE.spawn(world)
}
fun update(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f, pVel: Vector3f, pScale: Vector3f) {
val newProperties = pVelDE.properties
val scale = (max(max(pScale.x, pScale.y), pScale.z) + (pVel.x + pVel.y + pVel.z)) * 2.0f
val scaleVec3f = Vector3f(0.05f, scale, 0.05f)
val leftRotEuler = velToRot(pVel)
newProperties?.rot = rot
newProperties?.pos = eOrigin
newProperties?.translation = Vector3f(pOrigin)
newProperties?.leftRotation = eulerToQuat(leftRotEuler)
newProperties?.scale = scaleVec3f
pVelDE.updateProperties(newProperties!!)
}
fun kill() {
pVelDE.kill()
}
fun velToRot(velocity: Vector3f): Vector3f {
val normalizedVelocity = Vector3f(velocity).normalize()
val yaw = Math.toDegrees(atan2(normalizedVelocity.x.toDouble(), normalizedVelocity.z.toDouble())).toFloat()
val horizontalMag = sqrt(normalizedVelocity.x.toDouble().pow(2) + normalizedVelocity.z.toDouble().pow(2))
val pitch = Math.toDegrees(atan2(-normalizedVelocity.y.toDouble(), horizontalMag)).toFloat() + -90.0f
return Vector3f(pitch, yaw, 0.0f)
}
}
@@ -1,44 +1,91 @@
package com.bytedice.bde_particles.particles package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.Bde_particles
import com.bytedice.bde_particles.LIVING_PARTICLE_COUNT
import com.bytedice.bde_particles.randomFloatBetween
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3d
import org.joml.Vector2f
class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: ServerWorld, private val emitterParams: EmitterParams) { class ParticleEmitter(
var pos: Vec3d,
var rot: Vector2f,
val world: ServerWorld,
val params: EmitterParams,
private val debug: Boolean = false
) {
private var allParticles: Array<Particle> = emptyArray() private var allParticles: Array<Particle> = emptyArray()
private var timeAlive = 0 private var timeAlive = 0
private var loopCount = 0 private var timeEmitted = 0
private var loopDelay = 0 private var timePaused = 0
private var loopDur = 0 private var timesLooped = 0
private var loopDelayActive = false private var isPaused = false
private var isTicking = true private var isTicking = true
var isDead = false var isDead = false
private var debugTool = EmitterDebug(rot, pos, params.spawnPosOffset, params.shape)
fun tick() {
suspend fun tick() {
if (isTicking) { count() } if (isTicking) { count() }
if (isTicking) { addParticle() } if (isTicking) { addParticle() }
if (!isTicking && allParticles.isEmpty()) { isDead = true } if (!isTicking && allParticles.isEmpty()) { isDead = true; kill() }
for (particle in allParticles) { processParticlesInBatches(100)
if (particle.isDead) { allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray() }
else { particle.tick() } if (timeAlive == 0 && debug) { debugTool.spawn(world, pos, params.forceFields, params.spawnPosOffset) }
if (debug) { debugTool.update(rot, pos, params.spawnPosOffset) }
timeAlive += 1
} }
private suspend fun processParticlesInBatches(
batchSize: Int
) = coroutineScope {
val batches = allParticles.toMutableList().chunked(batchSize)
val jobs = batches.map { batch ->
async {
val toRemove = mutableListOf<Particle>()
batch.forEach { particle ->
if (particle.isDead) {
toRemove.add(particle)
} else {
particle.tick()
}
}
synchronized(allParticles) {
allParticles = allParticles.toMutableList().apply { removeAll(toRemove) }.toTypedArray()
}
}
}
jobs.awaitAll()
} }
private fun addParticle() { private fun addParticle() {
if (loopDelayActive) { return } if (isPaused) { return }
repeat(this.emitterParams.spawnsPerTick) { repeat(params.spawnRate) {
if (allParticles.size >= emitterParams.maxCount) { return } if (allParticles.size >= params.maxCount) { return }
val particle = Particle(emitterParams.particle) val maxParticles = world.gameRules.getInt(Bde_particles.GLOBAL_MAX_PARTICLES)
particle.init(this.emitterPos) if (LIVING_PARTICLE_COUNT >= maxParticles) { return }
particle.spawn(emitterWorld)
val randomSpawnChance = randomFloatBetween(0.0f, params.spawnChance.coerceIn(0.0f, 1.0f))
if (randomSpawnChance > params.spawnChance) { return }
val particle = Particle(params, pos, rot, debug)
particle.init()
particle.spawn(world)
allParticles += particle allParticles += particle
} }
@@ -46,44 +93,50 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S
private fun count() { private fun count() {
// this is the only section I have comments if (params.spawnDuration is ParamClasses.Duration.SingleBurst) {
// because im so damn good at cooking spaghetti val loopDur = (params.spawnDuration as ParamClasses.Duration.SingleBurst).loopDur
if (timeEmitted >= loopDur) { isTicking = false; return }
// if max duration is 0 or less, skip counting, its infinite timeEmitted += 1
if (emitterParams.loopDur <= 0) { return }
// if the duration is greater than max duration, add 1 loopCount
// return if max loopCount is 0
// if the delay is greater than 0 then enable it
if (loopDur >= emitterParams.loopDur) {
if (loopCount == 0) {
isTicking = false
return
}
loopCount += 1
loopDur = 0
if (emitterParams.loopDelay > 0) { loopDelayActive = true }
} }
// if the delay is greater than max delay, disable the delay else if (params.spawnDuration is ParamClasses.Duration.MultiBurst) {
else if (loopDelay >= emitterParams.loopDelay) { val loopDur = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopDur
loopDelay = 0 val loopDelay = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopDelay
loopDelayActive = false val loopCount = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopCount
if (timeEmitted >= loopDur && timesLooped + 1 >= loopCount) { isTicking = false; return }
if (timePaused >= loopDelay) {
isPaused = false
timePaused = 0
timesLooped += 1
}
if (timeEmitted >= loopDur && loopDelay > 0) {
isPaused = true
timeEmitted = 0
} }
// if loopCount is greater than max loopCount then stop immediately if (!isPaused) { timeEmitted += 1 }
// if max loopCount is below 0 then don't do anything, loop infinitely else { timePaused += 1 }
if (loopCount > 0 && loopCount >= emitterParams.loopCount) {
isTicking = false
return
} }
// if delay isn't active, add 1 to duration else if (params.spawnDuration is ParamClasses.Duration.InfiniteLoop) {
// if delay is active, add 1 to delay val loopDur = (params.spawnDuration as ParamClasses.Duration.InfiniteLoop).loopDur
if (!loopDelayActive) { loopDur += 1 } val loopDelay = (params.spawnDuration as ParamClasses.Duration.InfiniteLoop).loopDelay
else { loopDelay += 1 }
timeAlive += 1 if (timePaused >= loopDelay) {
isPaused = false
timePaused = 0
}
if (timeEmitted >= loopDur && loopDelay > 0) {
isPaused = true
timeEmitted = 0
}
if (!isPaused) { timeEmitted += 1 }
else { timePaused += 1 }
}
} }
@@ -92,7 +145,13 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S
particle.kill() particle.kill()
allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray() allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray()
} }
if (debug) { debugTool.kill() }
isTicking = false isTicking = false
isDead = true isDead = true
} }
fun stopTicking() {
isTicking = false
}
} }
@@ -1,134 +1,97 @@
@file:Suppress("UNCHECKED_CAST")
package com.bytedice.bde_particles.particles package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.InterpolationCurves import kotlin.reflect.KClass
import org.joml.Vector3f import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
import kotlin.reflect.full.isSubclassOf
val emitterIdRegister: MutableMap<String, EmitterParams> = mutableMapOf() val idRegister: MutableMap<String, EmitterParams> = mutableMapOf()
val forbiddenIds = arrayOf("", "DEFAULT", "NULL") val forbiddenIds = arrayOf("", "NULL")
fun addToEmitterRegister(id: String, params: EmitterParams) : Pair<String, Boolean> { fun addToRegister(id: String, params: EmitterParams) : Pair<String, Boolean> {
val newId = id.replace(" ", "_") val newId = replaceForbiddenChars(id.replace(" ", "_"))
if (newId in forbiddenIds && newId != "DEFAULT") { return Pair(newId, false) } if (idRegister.containsKey(newId)) {
if (emitterIdRegister.containsKey(newId)) { return Pair(newId, false) } println("BPS - Failed to register Emitter ID \"$newId\" as it already exists!")
return Pair(newId, false)
}
emitterIdRegister[id] = params if (newId in forbiddenIds) {
println("BPS - Failed to register Emitter ID \"$newId\" as it's been reserved for system use!")
return Pair(newId, false)
}
val newModels: MutableList<String> = mutableListOf()
for (model in params.modelCurve.array) {
val modelModIdRegex = ".*:".toRegex()
if (modelModIdRegex.containsMatchIn(model)) { newModels.add(model) }
else { newModels.add("minecraft:$model") }
}
params.modelCurve.array = newModels.toTypedArray()
idRegister[newId] = params
println("BPS - Registered Emitter ID \"$newId\".")
return Pair(newId, true) return Pair(newId, true)
} }
fun removeFromEmitterRegister(id: String) : Boolean { fun removeFromRegister(id: String) : Boolean {
if (id in forbiddenIds) { return false } if (!idRegister.containsKey(id)) {
if (!emitterIdRegister.containsKey(id)) { return false } println("BPS - Failed to remove Emitter ID \"$id\" as it doesn't exist!")
return false
}
if (id in forbiddenIds) {
println("BPS - Failed to remove Emitter ID \"$id\" as it's been reserved for system use!")
return false
}
emitterIdRegister.remove(id) idRegister.remove(id)
println("BPS - Removed Emitter ID \"$id\" from register.")
return true return true
} }
fun getEmitterParams(id: String) : EmitterParams? { fun getEmitterDataById(id: String) : EmitterParams? {
return emitterIdRegister[id] return idRegister[id]
} }
fun updateEmitterParams(id: String, newParams: EmitterParams) { fun updateRegistered(id: String, newParams: EmitterParams) {
emitterIdRegister[id] = newParams idRegister[id] = newParams
} }
fun updateSingleEmitterParam(id: String, paramName: String, paramValue: Any) : Boolean { fun updateParam(id: String, paramAccess: KProperty1<EmitterParams, *>, paramValue: Any) : Boolean {
val emitter = getEmitterParams(id) ?: return false val emitter = getEmitterDataById(id) ?: return false
val updatedValues = updateSingleEmitterParam(emitter, paramName, paramValue) val updatedValues = updateParams(emitter, paramAccess as KProperty1<EmitterParams, Any>, paramValue)
updateEmitterParams(id, updatedValues.first)
updateRegistered(id, updatedValues.first)
return updatedValues.second return updatedValues.second
} }
fun updateSingleParticleParam(emitterId: String, paramName: String, paramValue: Any) : Boolean { fun updateParams(params: EmitterParams, paramAccess: KProperty1<EmitterParams, Any>, paramValue: Any): Pair<EmitterParams, Boolean> {
val emitter = getEmitterParams(emitterId) ?: return false
val particle = emitter.particle
val updatedValues = updateSingleParticleParam(particle, paramName, paramValue)
emitter.particle = updatedValues.first
updateEmitterParams(emitterId, emitter)
return updatedValues.second
}
fun updateSingleEmitterParam(params: EmitterParams, paramName: String, paramValue: Any) : Pair<EmitterParams, Boolean> {
val value: Any? = when (paramName) {
"maxCount" -> paramValue as Int
"spawnsPerTick" -> paramValue as Int
"loopDur" -> paramValue as Int
"loopDelay" -> paramValue as Int
"loopCount" -> paramValue as Int
else -> null
}
if (value == null) { return Pair(params, false) }
val newParams = params.copy() val newParams = params.copy()
when (paramName) { if (paramAccess.returnType.classifier == paramValue::class
"maxCount" -> newParams.maxCount = value as Int || (paramAccess.returnType.classifier as? KClass<*>)?.let { paramValue::class.isSubclassOf(it) } == true
"spawnsPerTick" -> newParams.spawnsPerTick = value as Int )
"loopDur" -> newParams.loopDur = value as Int {
"loopDelay" -> newParams.loopDelay = value as Int paramAccess as KMutableProperty1<EmitterParams, Any>
"loopCount" -> newParams.loopCount = value as Int paramAccess.set(newParams, paramValue)
else -> return Pair(params, false) return Pair(newParams, true)
} }
return Pair(newParams, true) return Pair(params, false)
} }
fun updateSingleParticleParam(params: ParticleParams, paramName: String, paramValue: Any) : Pair<ParticleParams, Boolean> { fun replaceForbiddenChars(input: String): String {
val value: Any? = when (paramName) { val regex = "[^a-zA-Z0-9_-]".toRegex()
"shape" -> paramValue as SpawningShape? return input.replace(regex, "-")
"blockCurve" -> paramValue as Array<String>
"rotRandom" -> paramValue as Pair<Vector3f, Vector3f>
"rotVelRandom" -> paramValue as Pair<Vector3f, Vector3f>
"sizeRandom" -> paramValue as Pair<Vector3f, Vector3f>
"uniformSize" -> paramValue as Boolean
"velRandom" -> paramValue as Pair<Vector3f, Vector3f>
"forceFields" -> paramValue as Array<ForceField>
"gravity" -> paramValue as Vector3f
"drag" -> paramValue as Float
"minVel" -> paramValue as Float
"lifeTime" -> paramValue as Pair<Int, Int>
"rotVelCurve" -> paramValue as Triple<Vector3f, Vector3f, InterpolationCurves>
"sizeCurve" -> paramValue as Triple<Vector3f, Vector3f, InterpolationCurves>
else -> null
}
if (value == null) { return Pair(params, false) }
val newParams = params.copy()
when (paramName) {
"shape" -> newParams.shape = value as SpawningShape
"blockCurve" -> newParams.blockCurve = value as Array<String>
"rotRandom" -> newParams.rotRandom = value as Pair<Vector3f, Vector3f>
"rotVelRandom" -> newParams.rotVelRandom = value as Pair<Vector3f, Vector3f>
"sizeRandom" -> newParams.sizeRandom = value as Pair<Vector3f, Vector3f>
"uniformSize" -> newParams.uniformSize = value as Boolean
"velRandom" -> newParams.velRandom = value as Pair<Vector3f, Vector3f>
"forceFields" -> newParams.forceFields = value as Array<ForceField>
"gravity" -> newParams.gravity = value as Vector3f
"drag" -> newParams.drag = value as Float
"minVel" -> newParams.minVel = value as Float
"lifeTime" -> newParams.lifeTime = value as Pair<Int, Int>
"rotVelCurve" -> newParams.rotVelCurve = value as Triple<Vector3f, Vector3f, InterpolationCurves>
"sizeCurve" -> newParams.sizeCurve = value as Triple<Vector3f, Vector3f, InterpolationCurves>
else -> return Pair(params, false)
}
return Pair(newParams, true)
} }
@@ -1,75 +0,0 @@
package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.InterpolationCurves
import org.joml.Vector3f
/**
* ParticleParams defines the parameters for particle behavior, including shape, size, velocity, and other physics properties.
*
* @param shape The shape of the particle's spawning area, `null` is a single point.
* @param blockCurve An array of block names that gets cycled through during the particle's lifetime. Empty defaults to "minecraft:air".
* @param rotRandom The random ***initial*** rotation range for the particle's rotation in X, Y, and Z axes.
* @param rotVelRandom The random velocity for particle rotation in the X, Y, and Z axes.
* @param sizeRandom The random ***initial*** size range for the particle. If `uniformSize` is true, this will be uniform (cubic).
* @param uniformSize If true, particles will have a uniform (cubic) size. Otherwise, their sizes on different axis can vary.
* @param velRandom The random ***initial*** velocity range for the particle in the X, Y, and Z axes.
* @param forceFields An array of force fields applied to the particle.
* @param gravity A constant force applied to the particle every tick. It's often used as gravity.
* @param drag A drag force that affects the speed overtime. The formula is (velocity * (1.0 - drag))
* @param minVel If particles travel slower than this their speed is automatically 0. Values less than or equal to 0 mean the particle can travel at any speed.
* @param lifeTime A range for the particle's lifetime in ticks. A value below 1 will result in the particle not spawning.
* @param rotVelCurve A curve which the rotation velocity gets multiplied by during the particle's lifetime.
* @param sizeCurve A curve which the size gets multiplied by during the particle's lifetime.
*/
data class ParticleParams (
var shape: SpawningShape = SpawningShape.CIRCLE(3.0f),
var blockCurve: Array<String> = arrayOf("minecraft:purple_concrete", "minecraft:purple_stained_glass"),
var rotRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
var rotVelRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)),
var sizeRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)),
var uniformSize: Boolean = true,
var velRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(-0.1f, 0.3f, -0.1f), Vector3f(0.1f, 0.6f, 0.1f)),
var forceFields: Array<ForceField> = emptyArray(),
var gravity: Vector3f = Vector3f(0.0f, -0.01f, 0.0f),
var drag: Float = 0.075f,
var minVel: Float = 0.0f,
var lifeTime: Pair<Int, Int> = Pair(15, 45),
var rotVelCurve: Triple<Vector3f, Vector3f, InterpolationCurves> = Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f), InterpolationCurves.SQRT),
var sizeCurve: Triple<Vector3f, Vector3f, InterpolationCurves> = Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.5f, 0.5f, 0.5f), InterpolationCurves.LINEAR),
) {
companion object Presets {
val DEFAULT = ParticleParams()
val FIRE_GEYSER = ParticleParams(
SpawningShape.POINT,
arrayOf("minecraft:shroomlight", "minecraft:orange_concrete", "minecraft:orange_stained_glass", "minecraft:gray_stained_glass", "minecraft:light_gray_stained_glass"),
Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)),
Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)),
true,
Pair(Vector3f(-0.1f, 0.4f, -0.1f), Vector3f(0.1f, 0.8f, 0.1f)),
emptyArray(),
Vector3f(0.0f, -0.01f, 0.0f),
0.075f,
0.0f,
Pair(15, 45),
Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f), InterpolationCurves.SQRT),
Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.5f, 0.5f, 0.5f), InterpolationCurves.SQRT)
)
val RING_EXPLOSION = ParticleParams(
SpawningShape.CIRCLE(0.1f),
arrayOf("minecraft:shroomlight", "minecraft:orange_concrete", "minecraft:orange_stained_glass", "minecraft:gray_stained_glass", "minecraft:light_gray_stained_glass"),
Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)),
Pair(Vector3f(10.0f, 10.0f, 10.0f), Vector3f(15.0f, 15.0f, 15.0f)),
true,
Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(0.0f, 0.0f, 0.0f)),
arrayOf(ForceField("init", Vector3f(0.0f, -0.0001f, 0.0f), ForceFieldShape.SPHERE(0.1f, Pair(4.0f, 4.0f)))),
Vector3f(0.0f, 0.0f, 0.0f),
0.0075f,
0.0f,
Pair(50, 100),
Triple(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f), InterpolationCurves.SQRT),
Triple(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(3.0f, 3.0f, 3.0f), InterpolationCurves.SQRT)
)
}
}
@@ -1,23 +1,136 @@
package com.bytedice.bde_particles.particles package com.bytedice.bde_particles.particles
import com.bytedice.bde_particles.randomFloatBetween
import com.bytedice.bde_particles.randomIntBetween
import org.joml.Vector2f import org.joml.Vector2f
import org.joml.Vector3f import org.joml.Vector3f
import kotlin.math.*
import kotlin.random.Random
sealed class SpawningShape { sealed class SpawningShape {
data class CIRCLE(val radius: Float = 1.0f) : SpawningShape() class Circle(val radius: Float, val spawnOnEdge: Boolean) : SpawningShape() {
data class RECT (val size: Vector2f = Vector2f(1.0f, 1.0f)) : SpawningShape() fun randomWithin(): Vector3f {
data class CUBE (val size: Vector3f = Vector3f(1.0f, 1.0f, 1.0f)) : SpawningShape() val randRadius = radius * sqrt(randomFloatBetween(0.0f, 1.0f))
data class SPHERE(val radius: Float = 1.0f) : SpawningShape() val randAngle = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
data object POINT : SpawningShape()
val randomPos = Vector3f(randRadius * cos(randAngle), 0.0f, randRadius * sin(randAngle))
return randomPos
}
fun randomOnEdge(): Vector3f {
val randAngle = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
return Vector3f(radius * cos(randAngle), 0.0f, radius * sin(randAngle))
}
}
class Rect(val size: Vector2f, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
return Vector3f(randomFloatBetween(0.0f, size.x), 0.0f, randomFloatBetween(0.0f, size.y))
}
fun randomOnEdge(): Vector3f {
val perimeter = 2 * (size.x + size.y)
val randPos = randomFloatBetween(0.0f, perimeter)
return when {
randPos <= size.x -> Vector3f(randPos, 0.0f, 0.0f)
randPos <= size.x + size.y -> Vector3f(size.x, 0.0f, randPos - size.x)
randPos <= 2 * size.x + size.y -> Vector3f(size.x - (randPos - size.x - size.y), 0.0f, size.y)
else -> Vector3f(0.0f, 0.0f, size.y - (randPos - 2 * size.x - size.y))
}
}
}
class Cube(val size: Vector3f, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
return Vector3f(
randomFloatBetween(0.0f, size.x),
randomFloatBetween(0.0f, size.y),
randomFloatBetween(0.0f, size.z)
)
}
fun randomOnEdge(): Vector3f {
val edgeIndex = randomIntBetween(0, 12) // 12 edges
val randPos = randomFloatBetween(0.0f, 1.0f)
return when (edgeIndex) {
0 -> Vector3f(randPos * size.x, 0.0f, 0.0f)
1 -> Vector3f(size.x, 0.0f, randPos * size.z)
2 -> Vector3f(size.x - randPos * size.x, 0.0f, size.z)
3 -> Vector3f(0.0f, 0.0f, size.z - randPos * size.z)
4 -> Vector3f(randPos * size.x, size.y, 0.0f)
5 -> Vector3f(size.x, size.y, randPos * size.z)
6 -> Vector3f(size.x - randPos * size.x, size.y, size.z)
7 -> Vector3f(0.0f, size.y, size.z - randPos * size.z)
8 -> Vector3f(0.0f, randPos * size.y, 0.0f)
9 -> Vector3f(size.x, randPos * size.y, 0.0f)
10 -> Vector3f(size.x, randPos * size.y, size.z)
11 -> Vector3f(0.0f, randPos * size.y, size.z)
else -> Vector3f(0.0f, 0.0f, 0.0f)
}
}
}
class Sphere(val radius: Float, val spawnOnEdge: Boolean) : SpawningShape() {
fun randomWithin(): Vector3f {
val randRadius = radius * Random.nextDouble().pow(1.0 / 3.0)
val phi = Random.nextDouble(0.0, 2 * Math.PI)
val cosTheta = Random.nextDouble(-1.0, 1.0)
val theta = acos(cosTheta)
val x = randRadius * sin(theta) * cos(phi)
val y = randRadius * sin(theta) * sin(phi)
val z = randRadius * cos(theta)
return Vector3f(x.toFloat(), y.toFloat(), z.toFloat())
}
fun randomOnEdge(): Vector3f {
val phi = randomFloatBetween(0.0f, Math.PI.toFloat() * 2)
val cosTheta = randomFloatBetween(-1.0f, 1.0f)
val sinTheta = sqrt(1 - cosTheta * cosTheta)
val x = radius * sinTheta * cos(phi)
val y = radius * sinTheta * sin(phi)
val z = radius * cosTheta
return Vector3f(x, y, z)
}
}
data object Point : SpawningShape()
fun random(shape: SpawningShape) : Vector3f {
return when (shape) {
is Circle -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }
is Sphere -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }
is Rect -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }.sub(Vector3f(shape.size.x, 0.0f, shape.size.y).div(2.0f))
is Cube -> if (!shape.spawnOnEdge) { shape.randomWithin() } else { shape.randomOnEdge() }.sub(Vector3f(shape.size.x, shape.size.y, shape.size.z).div(2.0f))
is Point -> Vector3f(0.0f, 0.0f, 0.0f)
}
}
} }
sealed class ForceFieldShape { sealed class ForceFieldShape {
data class SPHERE( data class Sphere(
val radius: Float = 3.0f, val radius: Float,
val force: Pair<Float, Float> = Pair(0.0f, 0.02f), val force: Pair<Float, Float>,
) : ForceFieldShape() ) : ForceFieldShape()
data class CUBE(
val size: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), data class Cube(
val forceDir: Vector3f = Vector3f(0.0f, 0.0f, 0.0f) val size: Vector3f,
val force: Vector3f
) : ForceFieldShape() ) : ForceFieldShape()
fun deepCopy() : ForceFieldShape {
return when (this) {
is Sphere -> Sphere(this.radius, this.force)
is Cube -> Cube(Vector3f(this.size), Vector3f(this.force))
}
}
} }