aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-01-20 09:49:36 +0100
committernea <nea@nea.moe>2023-01-31 21:20:18 +0100
commit9fabe241cfe613b836335aa918b1a2423bfde31b (patch)
tree8e6a7d463e9888e28d6db31f0eebd7162f22a3b9
parent291860435d5487562b122aaddbe57c178a8734de (diff)
downloadNotEnoughUpdates-9fabe241cfe613b836335aa918b1a2423bfde31b.tar.gz
NotEnoughUpdates-9fabe241cfe613b836335aa918b1a2423bfde31b.tar.bz2
NotEnoughUpdates-9fabe241cfe613b836335aa918b1a2423bfde31b.zip
brigadier wip
-rw-r--r--build.gradle.kts3
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java7
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt64
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt31
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/Rome.kt241
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt133
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt28
7 files changed, 506 insertions, 1 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index e41aa513..4dc253a9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -139,6 +139,8 @@ dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.24")
"oneconfigAnnotationProcessor"("org.projectlombok:lombok:1.18.24")
+ shadowImplementation("com.mojang:brigadier:1.0.18")
+
shadowImplementation("org.spongepowered:mixin:0.7.11-SNAPSHOT") {
isTransitive = false // Dependencies of mixin are already bundled by minecraft
}
@@ -233,6 +235,7 @@ tasks.shadowJar {
from(kotlinDependencyCollectionJar)
dependsOn(kotlinDependencyCollectionJar)
fun relocate(name: String) = relocate(name, "io.github.moulberry.notenoughupdates.deps.$name")
+ relocate("com.mojang.brigadier")
}
tasks.assemble.get().dependsOn(remapJar)
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java
index cf266dca..3a2c39ca 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java
@@ -19,6 +19,8 @@
package io.github.moulberry.notenoughupdates.commands.dev;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import io.github.moulberry.notenoughupdates.BuildFlags;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import io.github.moulberry.notenoughupdates.commands.ClientCommandBase;
@@ -49,9 +51,12 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
+import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
+import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument;
+
public class DevTestCommand extends ClientCommandBase {
- private static final List<String> DEV_TESTERS =
+ public static final List<String> DEV_TESTERS =
Arrays.asList(
"d0e05de7-6067-454d-beae-c6d19d886191", // moulberry
"66502b40-6ac1-4d33-950d-3df110297aab", // lucycoconut
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt
new file mode 100644
index 00000000..14b6ed6e
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util.brigadier
+
+import com.mojang.brigadier.LiteralMessage
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType
+import com.mojang.brigadier.suggestion.Suggestions
+import com.mojang.brigadier.suggestion.SuggestionsBuilder
+import java.util.concurrent.CompletableFuture
+
+class EnumArgumentType<T : Enum<T>>(
+ val values: List<T>
+) : ArgumentType<T> {
+ companion object {
+ @JvmStatic
+ fun <T : Enum<T>> enum(values: Array<T>) = EnumArgumentType(values.toList())
+
+ inline fun <reified T : Enum<T>> enum() = enum(enumValues<T>())
+ }
+
+ override fun getExamples(): Collection<String> {
+ return values.map { it.name }
+ }
+
+ override fun <S : Any?> listSuggestions(
+ context: CommandContext<S>,
+ builder: SuggestionsBuilder
+ ): CompletableFuture<Suggestions> {
+
+ examples
+ .filter {builder.remaining.isBlank() || it.startsWith(builder.remaining, ignoreCase = true) }
+ .forEach { builder.suggest(it) }
+ return builder.buildFuture()
+ }
+
+ private val invalidEnum =
+ SimpleCommandExceptionType(LiteralMessage("Expected one of: ${values.joinToString(", ")}"))
+
+ override fun parse(reader: StringReader): T {
+ val enumName = reader.readString()
+ return values.find { enumName == it.name }
+ ?: throw invalidEnum.createWithContext(reader)
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt
new file mode 100644
index 00000000..adfdae6a
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util.brigadier
+
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+
+object RestArgumentType : ArgumentType<String> {
+ override fun parse(reader: StringReader): String {
+ val remaining = reader.remaining
+ reader.cursor += remaining.length
+ return remaining
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/Rome.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/Rome.kt
new file mode 100644
index 00000000..7f8f3f74
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/Rome.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2023 Linnea Gräf
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util.brigadier
+
+import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.ParseResults
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import com.mojang.brigadier.exceptions.CommandSyntaxException
+import com.mojang.brigadier.suggestion.Suggestions
+import io.github.moulberry.notenoughupdates.BuildFlags
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.commands.dev.DevTestCommand.DEV_TESTERS
+import io.github.moulberry.notenoughupdates.core.config.GuiPositionEditor
+import io.github.moulberry.notenoughupdates.core.util.MiscUtils
+import io.github.moulberry.notenoughupdates.miscfeatures.FishingHelper
+import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.CustomBiomes
+import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.LocationChangeEvent
+import io.github.moulberry.notenoughupdates.miscgui.GuiPriceGraph
+import io.github.moulberry.notenoughupdates.miscgui.minionhelper.MinionHelperManager
+import io.github.moulberry.notenoughupdates.util.LRUCache
+import io.github.moulberry.notenoughupdates.util.PronounDB
+import io.github.moulberry.notenoughupdates.util.SBInfo
+import io.github.moulberry.notenoughupdates.util.TabListUtils
+import io.github.moulberry.notenoughupdates.util.brigadier.EnumArgumentType.Companion.enum
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.command.CommandBase
+import net.minecraft.command.ICommandSender
+import net.minecraft.entity.player.EntityPlayer
+import net.minecraft.launchwrapper.Launch
+import net.minecraft.util.BlockPos
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.EnumChatFormatting
+import net.minecraft.util.EnumChatFormatting.GOLD
+import net.minecraft.util.EnumChatFormatting.RED
+import net.minecraft.util.EnumParticleTypes
+import net.minecraftforge.client.ClientCommandHandler
+import net.minecraftforge.common.MinecraftForge
+import java.util.concurrent.CompletableFuture
+
+@NEUAutoSubscribe
+object Rome {
+ val dispatcher = CommandDispatcher<DefaultSource>()
+ private val parseText =
+ LRUCache.memoize<Pair<ICommandSender, String>, ParseResults<DefaultSource>>({ (sender, text) ->
+ dispatcher.parse(text, sender)
+ }, 1)
+
+ init {
+ dispatcher.register(literal("neudevtest2") {
+ requires {
+ DEV_TESTERS.contains((it as? EntityPlayer)?.uniqueID?.toString())
+ || Launch.blackboard.get("fml.deobfuscatedEnvironment") as Boolean
+ }
+ thenLiteralExecute("profileinfo") {
+ val currentProfile = SBInfo.getInstance().currentProfile
+ val gamemode = SBInfo.getInstance().getGamemodeForProfile(currentProfile)
+ reply("${GOLD}You are on Profile $currentProfile with the mode $gamemode")
+ }
+ thenLiteralExecute("buildflags") {
+ reply("BuildFlags: \n" +
+ BuildFlags.getAllFlags().entries
+ .joinToString(("\n")) { (key, value) -> " + $key - $value" })
+ }
+ thenLiteral("exteditor") {
+ thenExecute {
+ reply("Your external editor is: §Z${NotEnoughUpdates.INSTANCE.config.hidden.externalEditor}")
+ }
+ thenArgument("editor", string()) { newEditor ->
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.config.hidden.externalEditor = this[newEditor]
+ reply("You changed your external editor to: §Z${this[newEditor]}")
+ }
+ }
+ }
+ thenLiteral("pricetest") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.manager.auctionManager.updateBazaar()
+ }
+ thenArgument("item", string()) { item ->
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = GuiPriceGraph(this[item])
+ }
+ }
+ }
+ thenLiteralExecute("zone") {
+ val target = Minecraft.getMinecraft().objectMouseOver.blockPos
+ ?: Minecraft.getMinecraft().thePlayer.position
+ val zone = CustomBiomes.INSTANCE.getSpecialZone(target)
+ listOf(
+ ChatComponentText("Showing Zone Info for: $target"),
+ ChatComponentText("Zone: " + (zone?.name ?: "null")),
+ ChatComponentText("Location: " + SBInfo.getInstance().getLocation()),
+ ChatComponentText("Biome: " + CustomBiomes.INSTANCE.getCustomBiome(target))
+ ).forEach { component ->
+ reply(component)
+ }
+ MinecraftForge.EVENT_BUS.post(
+ LocationChangeEvent(
+ SBInfo.getInstance().getLocation(), SBInfo
+ .getInstance()
+ .getLocation()
+ )
+ )
+ }
+ thenLiteralExecute("positiontest") {
+ NotEnoughUpdates.INSTANCE.openGui = GuiPositionEditor()
+ }
+ thenLiteral("pt") {
+ thenArgument("particle", enum<EnumParticleTypes>()) { particle ->
+ thenExecute {
+ FishingHelper.type = this[particle]
+ reply("Fishing particles set to ${FishingHelper.type}")
+ }
+ }
+ }
+ thenLiteralExecute("dev") {
+ NotEnoughUpdates.INSTANCE.config.hidden.dev = !NotEnoughUpdates.INSTANCE.config.hidden.dev
+ reply("§e[NEU] Dev mode " + if (NotEnoughUpdates.INSTANCE.config.hidden.dev) "§aenabled" else "§cdisabled")
+ }
+ thenLiteralExecute("saveconfig") {
+ NotEnoughUpdates.INSTANCE.saveConfig()
+ reply("Config saved")
+ }
+ thenLiteralExecute("searchmode") {
+ NotEnoughUpdates.INSTANCE.config.hidden.firstTimeSearchFocus = true
+ reply(EnumChatFormatting.AQUA.toString() + "I would never search")
+ }
+ thenLiteralExecute("bluehair") {
+ PronounDB.test()
+ }
+ thenLiteral("opengui") {
+ thenArgument("class", string()) { className ->
+ thenExecute {
+ try {
+ NotEnoughUpdates.INSTANCE.openGui =
+ Class.forName(this[className]).newInstance() as GuiScreen
+ reply("Opening gui: " + NotEnoughUpdates.INSTANCE.openGui)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ reply("Failed to open this GUI.")
+ }
+ }
+ }
+ }
+ thenLiteralExecute("center") {
+ val x = Math.floor(Minecraft.getMinecraft().thePlayer.posX) + 0.5f
+ val z = Math.floor(Minecraft.getMinecraft().thePlayer.posZ) + 0.5f
+ Minecraft.getMinecraft().thePlayer.setPosition(x, Minecraft.getMinecraft().thePlayer.posY, z)
+ reply("Literal hacks")
+ }
+ thenLiteral("minion") {
+ thenArgument("args", RestArgumentType) { arg ->
+ thenExecute {
+ MinionHelperManager.getInstance().handleCommand(arrayOf("minion") + this[arg].split(" "))
+ }
+ }
+ }
+ thenLiteralExecute("copytablist") {
+ val tabList = TabListUtils.getTabList().joinToString("\n", postfix = "\n")
+ MiscUtils.copyToClipboard(tabList)
+ reply("Copied tablist to clipboard!")
+ }
+ })
+ updateHooks()
+ }
+
+ fun updateHooks() = registerHooks(ClientCommandHandler.instance)
+
+ fun registerHooks(handler: ClientCommandHandler) {
+ dispatcher.root.children.forEach { commandNode ->
+ if (commandNode.name in handler.commands) return@forEach
+ handler.registerCommand(object : CommandBase() {
+ override fun getCommandName(): String {
+ return commandNode.name
+ }
+
+ override fun getCommandUsage(sender: ICommandSender): String {
+ return dispatcher.getAllUsage(commandNode, sender, true).joinToString("\n")
+ }
+
+ fun getText(args: Array<out String>) = "${commandNode.name} ${args.joinToString(" ")}"
+
+ override fun processCommand(sender: ICommandSender, args: Array<out String>) {
+ val results = parseText.apply(sender to getText(args).trim())
+ try {
+ dispatcher.execute(results)
+ } catch (syntax: CommandSyntaxException) {
+ sender.addChatMessage(ChatComponentText("${RED}${syntax.message}"))
+ }
+ }
+
+ var lastCompletionText: String? = null
+ var lastCompletion: CompletableFuture<Suggestions>? = null
+
+ override fun addTabCompletionOptions(
+ sender: ICommandSender,
+ args: Array<out String>,
+ pos: BlockPos
+ ): List<String> {
+ val originalText = getText(args)
+ var lc: CompletableFuture<Suggestions>? = null
+ if (lastCompletionText == originalText) {
+ lc = lastCompletion
+ }
+ if (lc == null) {
+ lastCompletion?.cancel(true)
+ val results = parseText.apply(sender to originalText)
+ lc = dispatcher.getCompletionSuggestions(results)
+ }
+ lastCompletion = lc
+ lastCompletionText = originalText
+ val suggestions = lastCompletion?.getNow(null) ?: return emptyList()
+ return suggestions.list.map { it.text }
+ }
+
+ override fun canCommandSenderUseCommand(sender: ICommandSender): Boolean {
+ return true
+ }
+ })
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt
new file mode 100644
index 00000000..b054f6d9
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 Linnea Gräf
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util.brigadier
+
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.builder.ArgumentBuilder
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import com.mojang.brigadier.builder.RequiredArgumentBuilder
+import com.mojang.brigadier.context.CommandContext
+import io.github.moulberry.notenoughupdates.util.iterate
+import net.minecraft.command.ICommandSender
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.IChatComponent
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+import java.lang.reflect.TypeVariable
+
+
+typealias DefaultSource = ICommandSender
+
+fun literal(
+ name: String,
+ block: LiteralArgumentBuilder<DefaultSource>.() -> Unit
+): LiteralArgumentBuilder<DefaultSource> =
+ LiteralArgumentBuilder.literal<DefaultSource>(name).also(block)
+
+private fun normalizeGeneric(argument: Type): Class<*> {
+ return if (argument is Class<*>) {
+ argument
+ } else if (argument is TypeVariable<*>) {
+ normalizeGeneric(argument.bounds[0])
+ } else if (argument is ParameterizedType) {
+ normalizeGeneric(argument.rawType)
+ } else {
+ Any::class.java
+ }
+}
+
+data class TypeSafeArg<T : Any>(val name: String, val argument: ArgumentType<T>) {
+ val argClass by lazy {
+ argument.javaClass
+ .iterate<Class<in ArgumentType<T>>> {
+ it.superclass
+ }
+ .flatMap {
+ it.genericInterfaces.toList()
+ }
+ .filterIsInstance<ParameterizedType>()
+ .find { it.rawType == ArgumentType::class.java }!!
+ .let {
+ normalizeGeneric(it.actualTypeArguments[0])
+ }
+ }
+
+ @JvmName("getWithThis")
+ fun <S> CommandContext<S>.get(): T =
+ get(this)
+
+
+ fun <S> get(ctx: CommandContext<S>): T {
+ return ctx.getArgument(name, argClass) as T
+ }
+}
+
+fun <T : ICommandSender, C : CommandContext<T>> C.reply(component: IChatComponent) {
+ source.addChatMessage(component)
+}
+
+fun <T : ICommandSender, C : CommandContext<T>> C.reply(text: String) {
+ source.addChatMessage(ChatComponentText(text))
+}
+
+operator fun <T : Any, C : CommandContext<*>> C.get(arg: TypeSafeArg<T>): T {
+ return arg.get(this)
+}
+
+
+fun <T : Any> argument(
+ name: String,
+ argument: ArgumentType<T>,
+ block: RequiredArgumentBuilder<DefaultSource, T>.(TypeSafeArg<T>) -> Unit
+): RequiredArgumentBuilder<DefaultSource, T> =
+ RequiredArgumentBuilder.argument<DefaultSource, T>(name, argument).also { block(it, TypeSafeArg(name, argument)) }
+
+fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgument(
+ name: String,
+ argument: ArgumentType<AT>,
+ block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit
+): T = then(argument(name, argument, block))
+
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral(
+ name: String,
+ block: LiteralArgumentBuilder<DefaultSource>.() -> Unit
+): T =
+ then(literal(name, block))
+
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteralExecute(
+ name: String,
+ block: CommandContext<DefaultSource>.() -> Unit
+): T =
+ thenLiteral(name) {
+ thenExecute(block)
+ }
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.then(node: ArgumentBuilder<DefaultSource, *>, block: T.() -> Unit): T =
+ then(node).also(block)
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.thenExecute(block: CommandContext<DefaultSource>.() -> Unit): T =
+ executes {
+ block(it)
+ 1
+ }
+
+
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt
new file mode 100644
index 00000000..bcfe11aa
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util
+
+fun <T : Any> T.iterate(evolve: (T) -> T?): Sequence<T> = sequence {
+ var pointer: T? = this@iterate
+ while (pointer != null) {
+ yield(pointer)
+ pointer = evolve(pointer)
+ }
+}