diff --git a/src/main/kotlin/com/bytedice/bde_particles/Bde_particles.kt b/src/main/kotlin/com/bytedice/bde_particles/Bde_particles.kt index 8113cea..f82a02b 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/Bde_particles.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/Bde_particles.kt @@ -1,7 +1,6 @@ package com.bytedice.bde_particles import com.bytedice.bde_particles.commands.GiveParticleEmitter -import com.bytedice.bde_particles.commands.MakeEmitter2 import com.bytedice.bde_particles.commands.ManageEmitters import com.bytedice.bde_particles.items.getToolDetails import com.bytedice.bde_particles.math.raycastFromPlayer @@ -24,6 +23,8 @@ import net.minecraft.world.World // TODO: finish the particle ticking // TODO: save particle emitters in world file, or kill them on server restart (so they don't linger forever) // TODO: make custom commands to create particles easier (only temporarily saved) +// TODO: add interpolation curves (easing) +// TODO: kill all particles command var ALL_PARTICLE_EMITTERS: Array = emptyArray() @@ -64,7 +65,8 @@ class Bde_particles : ModInitializer { fun init() { - addToParticleRegister("DEBUG", ParticleEmitterParams()) + addToEmitterRegister("DEFAULT", ParticleEmitterParams.DEFAULT) + addToEmitterRegister("FIRE_GEYSER", ParticleEmitterParams.FIRE_GEYSER) } @@ -80,9 +82,9 @@ fun tick() { fun onRightClick(player: PlayerEntity, world: ServerWorld, hand: Hand) : TypedActionResult { val handItem = player.getStackInHand(hand) - val (isEmitterTool, particleId) = getToolDetails(handItem) + val (isEmitterTool, emitterId) = getToolDetails(handItem) val hitResult = raycastFromPlayer(player as ServerPlayerEntity, 50.0) - val emitterParams = getParticleEmitterParams(particleId) + val emitterParams = getParticleEmitterParams(emitterId) if ( !isEmitterTool diff --git a/src/main/kotlin/com/bytedice/bde_particles/commands/GiveParticleEmitter.kt b/src/main/kotlin/com/bytedice/bde_particles/commands/GiveParticleEmitter.kt index d732666..b1569d8 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/commands/GiveParticleEmitter.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/commands/GiveParticleEmitter.kt @@ -1,9 +1,11 @@ package com.bytedice.bde_particles.commands import com.bytedice.bde_particles.items.makeData +import com.bytedice.bde_particles.particle.emitterIdRegister import com.mojang.brigadier.Command import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.suggestion.SuggestionsBuilder import net.minecraft.command.CommandRegistryAccess import net.minecraft.command.argument.ItemStackArgumentType import net.minecraft.server.command.CommandManager @@ -12,29 +14,40 @@ import net.minecraft.text.Style import net.minecraft.text.Text import net.minecraft.text.TextColor import java.awt.Color +import java.util.concurrent.CompletableFuture object GiveParticleEmitter { fun register(dispatcher: CommandDispatcher, registryAccess: CommandRegistryAccess) { val command = CommandManager.literal("giveParticleEmitter") - .requires { source -> source.hasPermissionLevel(2) } + .requires { source -> source.hasPermissionLevel(4) } - val itemNameArg = CommandManager.argument("item name", StringArgumentType.string()) - val itemTypeArg = CommandManager.argument("item type", ItemStackArgumentType.itemStack(registryAccess)) - val particleIdArg = CommandManager.argument("particle id", StringArgumentType.string()) + val allEmitterIds = emitterIdRegister.keys + + val itemNameArg = CommandManager.argument("Item Name", StringArgumentType.string()) + val itemTypeArg = CommandManager.argument("Item Type", ItemStackArgumentType.itemStack(registryAccess)) + + val emitterIdSuggestion = CommandManager.argument("Emitter ID", StringArgumentType.string()) + .suggests { _, builder -> + val suggestionsBuilder = SuggestionsBuilder(builder.remaining, 0) + allEmitterIds.forEach { key -> + suggestionsBuilder.suggest(key) + } + CompletableFuture.completedFuture(suggestionsBuilder.build()) + } dispatcher.register( command.then( itemNameArg.then( itemTypeArg.then( - particleIdArg.executes { context -> - val name = StringArgumentType.getString(context, "item name") - val item = ItemStackArgumentType.getItemStackArgument(context, "item type") - val particleId = StringArgumentType.getString(context, "particle id") + emitterIdSuggestion.executes { context -> + val name = StringArgumentType.getString(context, "Item Name") + val item = ItemStackArgumentType.getItemStackArgument(context, "Item Type") + 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: \"${particleId}\"]") - .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 125, 0).rgb)).withItalic(true)) + 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)).withItalic(true)) - context.source.player?.inventory?.insertStack(makeData(item.item, name, particleId)) + context.source.player?.inventory?.insertStack(makeData(item.item, name, emitterId)) context.source.sendFeedback({ feedback }, false) Command.SINGLE_SUCCESS } diff --git a/src/main/kotlin/com/bytedice/bde_particles/commands/MakeEmitter.kt b/src/main/kotlin/com/bytedice/bde_particles/commands/MakeEmitter.kt deleted file mode 100644 index 8e3267d..0000000 --- a/src/main/kotlin/com/bytedice/bde_particles/commands/MakeEmitter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.bytedice.bde_particles.commands - -import com.bytedice.bde_particles.particle.particleIdRegister -import com.mojang.brigadier.Command -import com.mojang.brigadier.CommandDispatcher -import com.mojang.brigadier.arguments.StringArgumentType -import com.mojang.brigadier.suggestion.SuggestionsBuilder -import net.minecraft.server.command.CommandManager -import net.minecraft.server.command.ServerCommandSource -import net.minecraft.text.Text -import java.util.concurrent.CompletableFuture - - -// ManageEmitters -// -// create - // [emitter id] - // [preset emitter id] or DEFAULT - // output -> register new emitter with a preset -// config - // [emitter id] - // [particle index] - // [param key] - // [new param value] - // output -> update the selected parameter of the selected particle id to the new value - -object ManageEmitters { - fun register(dispatcher: CommandDispatcher) { - val command = CommandManager.literal("ManageEmitters") - .requires { source -> source.hasPermissionLevel(4) } - - val allParticleIds = particleIdRegister.keys - - val particleIdArg = CommandManager.argument("Particle ID", StringArgumentType.string()) - .suggests { _, builder -> - val suggestionsBuilder = SuggestionsBuilder(builder.remaining, 0) - allParticleIds.forEach { key -> - suggestionsBuilder.suggest(key) - } - CompletableFuture.completedFuture(suggestionsBuilder.build()) - } - - dispatcher.register( - command.then( - particleIdArg.executes { context -> - val particleId = StringArgumentType.getString(context, "Particle ID") - - val feedback = Text.literal("you chose Particle ID \"$particleId\"") - - context.source.sendFeedback({ feedback }, false) - Command.SINGLE_SUCCESS - } - ) - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/bytedice/bde_particles/commands/ManageEmitters.kt b/src/main/kotlin/com/bytedice/bde_particles/commands/ManageEmitters.kt new file mode 100644 index 0000000..79d1591 --- /dev/null +++ b/src/main/kotlin/com/bytedice/bde_particles/commands/ManageEmitters.kt @@ -0,0 +1,164 @@ +package com.bytedice.bde_particles.commands + +import com.bytedice.bde_particles.particle.addToEmitterRegister +import com.bytedice.bde_particles.particle.emitterIdRegister +import com.bytedice.bde_particles.particle.getParticleEmitterParams +import com.mojang.brigadier.Command +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.suggestion.SuggestionsBuilder +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 net.minecraft.text.TextColor +import java.awt.Color +import java.util.concurrent.CompletableFuture + + +// ManageEmitters +// + // create + // [emitter id] + // [preset emitter id] + // output -> register new emitter with a preset + + // remove + // [emitter id] + // output -> removes the particle with that id + + // config + // [emitter id] + // <[particle index] / EMITTER> + + // + // single param (if EMITTER, use emitter params instead) + // [param key] + // [new param value] + // output -> update the selected parameter of the selected emitter id to the new value + + // dataClass + // [new params] (unspecified values is treated as current) + // output -> update all params according to the dataClass + + // list + // output -> all registered emitters + + // copy + // output -> give player a command block with the particle params (should be paste-able in kotlin) + + +object ManageEmitters { + fun register(dispatcher: CommandDispatcher) { + val command = CommandManager.literal("ManageEmitters") + .requires { source -> source.hasPermissionLevel(4) } + + dispatcher.register( + command + // + .then(argCreate()) + .then(argRemove()) + .then(argConfig()) + .then(argList()) + .then(argCopy()) + ) + } +} + + +val allEmitterIds = emitterIdRegister.keys + +val emitterIdSuggestion = CommandManager.argument("Registered Emitter ID", StringArgumentType.string()) + .suggests { _, builder -> + val suggestionsBuilder = SuggestionsBuilder(builder.remaining, 0) + allEmitterIds.forEach { key -> + suggestionsBuilder.suggest(key) + } + CompletableFuture.completedFuture(suggestionsBuilder.build()) + } + + +fun argCreate() : LiteralArgumentBuilder { + return CommandManager.literal("create") + .then( + // [emitter id] + CommandManager.argument("Emitter ID", StringArgumentType.string()) + .then( + // [preset emitter id] + emitterIdSuggestion + .executes { context -> + val emitterIdVal = StringArgumentType.getString(context, "Emitter ID") + val emitterPresetVal = StringArgumentType.getString(context, "Registered Emitter ID") ?: "DEFAULT" + val emitterPresetParams = getParticleEmitterParams(emitterPresetVal) + + var newEmitterId = "NULL_EMITTER_ID" + if (emitterPresetParams != null) { + newEmitterId = addToEmitterRegister(emitterIdVal, emitterPresetParams).second + } + + val feedback = if (newEmitterId in allEmitterIds) { + 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(255, 0, 0).rgb))) + } + else { + 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))) + } + + context.source.sendFeedback( { feedback }, false ) + Command.SINGLE_SUCCESS + } + ) + ) +} + + +fun argRemove() : LiteralArgumentBuilder { + return CommandManager.literal("remove") + .executes { context -> + val feedback = Text.literal("remove") + context.source.sendFeedback({ feedback }, false) + Command.SINGLE_SUCCESS + } +} + + +fun argConfig() : LiteralArgumentBuilder { + return CommandManager.literal("config") + .executes { context -> + val feedback = Text.literal("remove") + context.source.sendFeedback({ feedback }, false) + Command.SINGLE_SUCCESS + } +} + + +fun argList() : LiteralArgumentBuilder { + return CommandManager.literal("list") + .executes { context -> + val feedback = Text.literal("remove") + context.source.sendFeedback({ feedback }, false) + Command.SINGLE_SUCCESS + } +} + + +fun argCopy() : LiteralArgumentBuilder { + return CommandManager.literal("copy") + .executes { context -> + val feedback = Text.literal("remove") + context.source.sendFeedback({ feedback }, false) + Command.SINGLE_SUCCESS + } +} + + +/* +fun argCopyPasta() : LiteralArgumentBuilder { + return +} +*/ \ No newline at end of file diff --git a/src/main/kotlin/com/bytedice/bde_particles/items/ParticleEmitterTool.kt b/src/main/kotlin/com/bytedice/bde_particles/items/ParticleEmitterTool.kt index 9011bf5..193c8ca 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/items/ParticleEmitterTool.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/items/ParticleEmitterTool.kt @@ -12,20 +12,20 @@ import net.minecraft.text.TextColor import java.awt.Color -fun makeData(itemId: Item, name: String, particleId: String): ItemStack { +fun makeData(itemId: Item, name: String, emitterId: String): ItemStack { val i = ItemStack(itemId) val nbt = NbtCompound() nbt.putByte("BPS_particleTool", 1) - nbt.putString("BPS_particleId", particleId) + nbt.putString("BPS_emitterID", emitterId) val component: NbtComponent = NbtComponent.of(nbt) val displayName = Text.literal(name) - .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 125, 0).rgb)).withItalic(true)) + .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)).withItalic(true)) - val loreLines = Text.literal("BPS - Bound to id \"$particleId\"") - .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 125, 0).rgb)).withItalic(true)) + val loreLines = Text.literal("BPS - Bound to ID \"$emitterId\"") + .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(Color(0, 200, 0).rgb)).withItalic(true)) val lore = LoreComponent(listOf(loreLines)) @@ -40,13 +40,13 @@ fun makeData(itemId: Item, name: String, particleId: String): ItemStack { fun getToolDetails(item: ItemStack) : Pair { val customData = item.get(DataComponentTypes.CUSTOM_DATA) val isParticleEmitterTool = customData?.nbt?.getByte("BPS_particleTool") == 1.toByte() - val particleId = customData?.nbt?.getString("BPS_particleId") + val emitterId = customData?.nbt?.getString("BPS_emitterID") - return if (particleId == null) { + return if (emitterId == null) { Pair(isParticleEmitterTool, "") } else { - Pair(isParticleEmitterTool, particleId) + Pair(isParticleEmitterTool, emitterId) } } \ No newline at end of file diff --git a/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitter.kt b/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitter.kt index ce870e4..475320b 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitter.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitter.kt @@ -19,11 +19,9 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S fun tick() { - if (isCounting) { count() } + if (isCounting) { count(); addParticle() } if (!isCounting && allParticles.isEmpty()) { isDead = true } - addParticle() - for (particle in allParticles) { if (particle.isDead) { allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray() } else { particle.tick() } @@ -36,7 +34,7 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S for (i in emitterParams.particleTypes.indices) { repeat(this.emitterParams.spawnsPerTick) { - if (allParticles.size < emitterParams.maxCount) { return } + if (allParticles.size >= emitterParams.maxCount) { return } val particle = Particle(emitterParams.particleTypes[i]) particle.init(this.emitterPos) @@ -49,7 +47,6 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S private fun count() { - // TODO: this is broken or something, no particles are spawning // this is the only section I have comments // because im so damn good at cooking spaghetti @@ -57,8 +54,13 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S 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) { + isCounting = false + return + } loopCount += 1 loopDur = 0 if (emitterParams.loopDelay > 0) { loopDelayActive = true } @@ -71,9 +73,8 @@ class ParticleEmitter(private val emitterPos: Vec3d, private val emitterWorld: S } // if loopCount is greater than max loopCount then stop immediately - // only loops once if max loopCount is 0 // if max loopCount is below 0 then don't do anything, loop infinitely - if (emitterParams.loopCount in 0.. 0 && emitterParams.loopCount >= loopCount) { isCounting = false return } diff --git a/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitterParams.kt b/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitterParams.kt index 6c81a29..5620b4f 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitterParams.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleEmitterParams.kt @@ -1,21 +1,51 @@ package com.bytedice.bde_particles.particle +import org.joml.Vector3f + /** * ParticleEmitterParams defines the parameters for controlling how particles are emitted, * 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 particleTypes An array of `ParticleParams` defining the types of particles to spawn. If empty, 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. This only takes effect if `loop` is true. 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 particleTypes An array of `ParticleParams` defining the types of particles to spawn. If empty, no particles will spawn. */ data class ParticleEmitterParams ( - val maxCount: Int = 25, - val spawnsPerTick: Int = 25, - val particleTypes: Array = emptyArray(), + val maxCount: Int = 200, + val spawnsPerTick: Int = 2, val loopDur: Int = 25, val loopDelay: Int = 10, val loopCount: Int = 3, -) \ No newline at end of file + val particleTypes: Array = arrayOf(ParticleParams()), +) +{ + companion object Presets { + val DEFAULT = ParticleEmitterParams() + val FIRE_GEYSER = ParticleEmitterParams( + 200, + 1, + 25, + 0, + 0, + arrayOf(ParticleParams( + null, + 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)), + arrayOf(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f)), + Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)), + true, + arrayOf(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.5f, 0.5f, 0.5f)), + 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) + )) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleParams.kt b/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleParams.kt index dc557f9..c41540b 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleParams.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/particle/ParticleParams.kt @@ -6,7 +6,7 @@ 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. + * @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 rotVelCurve A curve which the rotation velocity gets multiplied by during the particle's lifetime. @@ -21,7 +21,7 @@ import org.joml.Vector3f * @param lifeTime A range for the particle's lifetime in ticks. A value below 1 will result in the particle not spawning. */ data class ParticleParams ( - val shape: SpawningShape? = SpawningShape.Circle(1.0f), + val shape: SpawningShape? = SpawningShape.Circle(3.0f), val blockCurve: Array = arrayOf("minecraft:shroomlight", "minecraft:orange_concrete", "minecraft:orange_stained_glass", "minecraft:gray_stained_glass", "minecraft:light_gray_stained_glass"), val rotRandom: Pair = Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)), val rotVelRandom: Pair = Pair(Vector3f(-0.2f, -0.2f, -0.2f), Vector3f(0.2f, 0.2f, 0.2f)), diff --git a/src/main/kotlin/com/bytedice/bde_particles/particle/particleIdRegister.kt b/src/main/kotlin/com/bytedice/bde_particles/particle/particleIdRegister.kt index e533161..0c4dbaa 100644 --- a/src/main/kotlin/com/bytedice/bde_particles/particle/particleIdRegister.kt +++ b/src/main/kotlin/com/bytedice/bde_particles/particle/particleIdRegister.kt @@ -1,32 +1,34 @@ package com.bytedice.bde_particles.particle -val particleIdRegister: MutableMap = mutableMapOf() +val emitterIdRegister: MutableMap = mutableMapOf() fun returnParticleFunction(id: String) : ParticleEmitterParams? { - if (particleIdRegister.containsKey(id)) { - return particleIdRegister[id] + if (emitterIdRegister.containsKey(id)) { + return emitterIdRegister[id] } return null } -fun addToParticleRegister(id: String, params: ParticleEmitterParams) : String { - if (!particleIdRegister.containsKey(id)) { - particleIdRegister[id] = params +fun addToEmitterRegister(id: String, params: ParticleEmitterParams) : Pair { + val newId = id.replace(" ", "_") - val msg = "Successfully added particle id \"$id\" to register" + if (!emitterIdRegister.containsKey(id)) { + emitterIdRegister[id] = params + + val msg = "BPS - Successfully added Emitter ID \"$newId\" to register" println(msg) - return msg + return Pair(msg, newId) } else { - val msg = "Particle id \"$id\" is already registered, please choose another id!" + val msg = "BPS - Emitter ID \"$newId\" is already registered, please choose another ID!" println(msg) - return msg + return Pair(msg, newId) } } fun getParticleEmitterParams(id: String) : ParticleEmitterParams? { - return particleIdRegister[id] + return emitterIdRegister[id] } \ No newline at end of file