Skip to content

Commit 973143c

Browse files
feat!: Use Cloud Command Framework (#540)
* feat: Use Cloud annotations for commands * refactor: Convert ArmorColorCommand to cloud * refactor: Convert CalcXPCommand to cloud * refactor: Convert GlintCustomizeCommand to cloud * remove: FragBotCommand * refactor: Convert OrderedWaypointCommand to cloud * refactor: Convert ProtectItemCommand to cloud * refactor: Convert RepartyCommand to cloud Override Reparty no longer does anything * refactor: Convert TrackCooldownCommand to cloud * refactor: Convert ItemCycleCommand to cloud * refactor: Convert HollowWaypointCommand to cloud * refactor: Convert ScamCheckCommand to cloud * refactor: Convert TrophyFishCommand to cloud * refactor: Convert CataCommand to cloud * refactor: Convert SlayerCommand to cloud * refactor: Convert SkytilsCommand to cloud * remove: BaseCommand and StatCommand * fix: Catch errors when constructing commands * fix!: HollowWaypointCommand Syntax Some things of note: Cloud doesn't seem to support greedy string, so you can't use names with spaces anymore Difference in argument order (cloud/brigadier limitation) may affect other mods (NEU) * fix: Make some commands greedy * fix: Make HollowWaypointCommand Quoted instead of Greedy * feat: Improve error presentation * fix: Use launch instead of withContext for commands The current command executor will run on the calling thread. We can try async later if we verify there are no concurrency issues. * refactor: Make it easier for future multiversioning * fix: Re-throw any caught exceptions while executing commands * feat: Backwards compatibility hack for `sthw add` when using NEU * fix: Remove duplicate exception messages by wrapping the exception
1 parent b4aa306 commit 973143c

27 files changed

+1470
-1068
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ dependencies {
166166
}
167167
compileOnly("org.bouncycastle:bcprov-jdk18on:1.78.1")
168168

169+
shadowMe("org.incendo:cloud-kotlin-coroutines-annotations:2.0.0") { excludeKotlin() }
170+
shadowMe("org.incendo:cloud-kotlin-extensions:2.0.0") { excludeKotlin() }
169171

170172
compileOnly("net.hypixel:mod-api-forge:1.0.1.2") {
171173
exclude(group = "me.djtheredstoner", module = "DevAuth-forge-legacy")

src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ package gg.skytils.skytilsmod
2121
import gg.essential.api.EssentialAPI
2222
import gg.essential.universal.UChat
2323
import gg.essential.universal.UKeyboard
24-
import gg.skytils.skytilsmod.commands.impl.*
25-
import gg.skytils.skytilsmod.commands.stats.impl.CataCommand
26-
import gg.skytils.skytilsmod.commands.stats.impl.SlayerCommand
24+
import gg.skytils.skytilsmod.commands.SkytilsCommands
2725
import gg.skytils.skytilsmod.core.*
2826
import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent
2927
import gg.skytils.skytilsmod.events.impl.PacketEvent
@@ -331,6 +329,8 @@ class Skytils {
331329
SoundQueue,
332330
UpdateChecker,
333331

332+
NEUCompatibility,
333+
334334
AlignmentTaskSolver,
335335
AntiFool,
336336
ArmorColor,
@@ -440,48 +440,7 @@ class Skytils {
440440

441441
@Mod.EventHandler
442442
fun loadComplete(event: FMLLoadCompleteEvent) {
443-
val cch = ClientCommandHandler.instance
444-
445-
if (cch !is AccessorCommandHandler) throw RuntimeException(
446-
"Skytils was unable to mixin to the CommandHandler. Please report this on our Discord at discord.gg/skytils."
447-
)
448-
cch.registerCommand(SkytilsCommand)
449-
450-
cch.registerCommand(CataCommand)
451-
cch.registerCommand(CalcXPCommand)
452-
cch.registerCommand(FragBotCommand)
453-
cch.registerCommand(HollowWaypointCommand)
454-
cch.registerCommand(ItemCycleCommand)
455-
cch.registerCommand(OrderedWaypointCommand)
456-
cch.registerCommand(ScamCheckCommand)
457-
cch.registerCommand(SlayerCommand)
458-
cch.registerCommand(TrophyFishCommand)
459-
460-
if (!cch.commands.containsKey("armorcolor")) {
461-
cch.registerCommand(ArmorColorCommand)
462-
}
463-
464-
if (!cch.commands.containsKey("glintcustomize")) {
465-
cch.registerCommand(GlintCustomizeCommand)
466-
}
467-
468-
if (!cch.commands.containsKey("protectitem")) {
469-
cch.registerCommand(ProtectItemCommand)
470-
}
471-
472-
if (!cch.commands.containsKey("trackcooldown")) {
473-
cch.registerCommand(TrackCooldownCommand)
474-
}
475-
476-
cch.commandSet.add(RepartyCommand)
477-
cch.commandMap["skytilsreparty"] = RepartyCommand
478-
if (config.overrideReparty || !cch.commands.containsKey("reparty")) {
479-
cch.commandMap["reparty"] = RepartyCommand
480-
}
481-
482-
if (config.overrideReparty || !cch.commands.containsKey("rp")) {
483-
cch.commandMap["rp"] = RepartyCommand
484-
}
443+
SkytilsCommands
485444

486445
if (UpdateChecker.currentVersion.specialVersionType != UpdateChecker.UpdateType.RELEASE && config.updateChannel == 2) {
487446
if (ModChecker.canShowNotifications) {

src/main/kotlin/gg/skytils/skytilsmod/commands/BaseCommand.kt

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Skytils - Hypixel Skyblock Quality of Life Mod
3+
* Copyright (C) 2020-2025 Skytils
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package gg.skytils.skytilsmod.commands
20+
21+
import gg.skytils.skytilsmod.commands.impl.*
22+
import gg.skytils.skytilsmod.commands.utils.legacy.LegacyForgeRegistrationHandler
23+
import net.minecraft.command.ICommandSender
24+
import org.incendo.cloud.CommandManager
25+
import org.incendo.cloud.annotations.AnnotationParser
26+
import org.incendo.cloud.execution.ExecutionCoordinator
27+
import org.incendo.cloud.kotlin.coroutines.annotations.installCoroutineSupport
28+
29+
object SkytilsCommands : CommandManager<SkytilsCommandSender>(ExecutionCoordinator.simpleCoordinator(),
30+
LegacyForgeRegistrationHandler
31+
) {
32+
val annotationParser = AnnotationParser(this, SkytilsCommandSender::class.java)
33+
34+
init {
35+
runCatching {
36+
annotationParser.installCoroutineSupport()
37+
38+
val parsedCommands = annotationParser.parse(
39+
ArmorColorCommand,
40+
CalcXPCommand,
41+
CataCommand,
42+
GlintCustomizeCommand,
43+
HollowWaypointCommand,
44+
ItemCycleCommand,
45+
OrderedWaypointCommand,
46+
ProtectItemCommand,
47+
RepartyCommand,
48+
ScamCheckCommand,
49+
SkytilsCommand,
50+
SlayerCommand,
51+
TrackCooldownCommand,
52+
TrophyFishCommand
53+
)
54+
println("Parsed ${parsedCommands.size} commands.")
55+
}.onFailure {
56+
it.printStackTrace()
57+
}
58+
}
59+
60+
override fun hasPermission(
61+
sender: ICommandSender,
62+
permission: String
63+
): Boolean = true
64+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Skytils - Hypixel Skyblock Quality of Life Mod
3+
* Copyright (C) 2020-2025 Skytils
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package gg.skytils.skytilsmod.commands
20+
21+
typealias SkytilsCommandSender = net.minecraft.command.ICommandSender

src/main/kotlin/gg/skytils/skytilsmod/commands/impl/ArmorColorCommand.kt

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,57 +19,67 @@ package gg.skytils.skytilsmod.commands.impl
1919

2020
import gg.essential.universal.UChat
2121
import gg.skytils.skytilsmod.Skytils.Companion.failPrefix
22-
import gg.skytils.skytilsmod.Skytils.Companion.prefix
22+
import gg.skytils.skytilsmod.Skytils.Companion.mc
2323
import gg.skytils.skytilsmod.Skytils.Companion.successPrefix
24-
import gg.skytils.skytilsmod.commands.BaseCommand
2524
import gg.skytils.skytilsmod.core.PersistentSave
2625
import gg.skytils.skytilsmod.features.impl.handlers.ArmorColor
2726
import gg.skytils.skytilsmod.utils.ItemUtil
2827
import gg.skytils.skytilsmod.utils.Utils
2928
import gg.skytils.skytilsmod.utils.graphics.colors.CustomColor
30-
import net.minecraft.client.entity.EntityPlayerSP
3129
import net.minecraft.command.SyntaxErrorException
3230
import net.minecraft.command.WrongUsageException
3331
import net.minecraft.item.ItemArmor
32+
import net.minecraft.item.ItemStack
33+
import org.incendo.cloud.annotation.specifier.Greedy
34+
import org.incendo.cloud.annotations.Argument
35+
import org.incendo.cloud.annotations.Command
36+
import org.incendo.cloud.annotations.Commands
3437

35-
object ArmorColorCommand : BaseCommand("armorcolor", listOf("armourcolour", "armorcolour", "armourcolor")) {
36-
override fun getCommandUsage(player: EntityPlayerSP): String = "/armorcolor <clearall/clear/set>"
38+
@Commands
39+
object ArmorColorCommand {
40+
@Command("armorcolor clearall")
41+
fun clearAll() {
42+
ArmorColor.armorColors.clear()
43+
PersistentSave.markDirty<ArmorColor>()
44+
UChat.chat("$successPrefix §aCleared all your custom armor colors!")
45+
}
3746

38-
override fun processCommand(player: EntityPlayerSP, args: Array<String>) {
39-
if (args.isEmpty()) {
40-
UChat.chat("$prefix §b" + getCommandUsage(player))
41-
return
42-
}
43-
val subcommand = args[0].lowercase()
44-
if (subcommand == "clearall") {
45-
ArmorColor.armorColors.clear()
47+
@Command("armorcolor clear")
48+
fun clearCurrent() {
49+
val (item, uuid) = getCurrentArmor()
50+
51+
if (ArmorColor.armorColors.containsKey(uuid)) {
52+
ArmorColor.armorColors.remove(uuid)
4653
PersistentSave.markDirty<ArmorColor>()
47-
UChat.chat("$successPrefix §aCleared all your custom armor colors!")
48-
} else if (subcommand == "clear" || subcommand == "set") {
49-
if (!Utils.inSkyblock) throw WrongUsageException("You must be in Skyblock to use this command!")
50-
val item = player.heldItem
51-
?: throw WrongUsageException("You must hold a leather armor piece to use this command")
52-
if ((item.item as? ItemArmor)?.armorMaterial != ItemArmor.ArmorMaterial.LEATHER) throw WrongUsageException("You must hold a leather armor piece to use this command")
53-
val extraAttributes = ItemUtil.getExtraAttributes(item)
54-
if (extraAttributes == null || !extraAttributes.hasKey("uuid")) throw WrongUsageException("This item does not have a UUID!")
55-
val uuid = extraAttributes.getString("uuid")
56-
if (subcommand == "set") {
57-
if (args.size != 2) throw WrongUsageException("You must specify a valid hex color!")
58-
val color: CustomColor = try {
59-
Utils.customColorFromString(args[1])
60-
} catch (e: IllegalArgumentException) {
61-
throw SyntaxErrorException("$failPrefix §cUnable to get a color from inputted string.")
62-
}
63-
ArmorColor.armorColors[uuid] = color
64-
PersistentSave.markDirty<ArmorColor>()
65-
UChat.chat("$successPrefix §aSet the color of your ${item.displayName}§a to ${args[1]}!")
66-
} else {
67-
if (ArmorColor.armorColors.containsKey(uuid)) {
68-
ArmorColor.armorColors.remove(uuid)
69-
PersistentSave.markDirty<ArmorColor>()
70-
UChat.chat("$successPrefix §aCleared the custom color for your ${item.displayName}§a!")
71-
} else UChat.chat("§cThat item doesn't have a custom color!")
72-
}
73-
} else UChat.chat(getCommandUsage(player))
54+
UChat.chat("$successPrefix §aCleared the custom color for your ${item.displayName}§a!")
55+
} else UChat.chat("§cThat item doesn't have a custom color!")
56+
}
57+
58+
@Command("armorcolor set <color>")
59+
fun setCurrent(
60+
@Greedy
61+
@Argument("color", description = "The color to set the armor to")
62+
color: String
63+
) {
64+
val (item, uuid) = getCurrentArmor()
65+
val customColor: CustomColor = try {
66+
Utils.customColorFromString(color)
67+
} catch (e: IllegalArgumentException) {
68+
throw SyntaxErrorException("$failPrefix §cUnable to get a color from inputted string.")
69+
}
70+
ArmorColor.armorColors[uuid] = customColor
71+
PersistentSave.markDirty<ArmorColor>()
72+
UChat.chat("$successPrefix §aSet the color of your ${item.displayName}§a to $color!")
73+
}
74+
75+
private fun getCurrentArmor(): Pair<ItemStack, String> {
76+
if (!Utils.inSkyblock) throw WrongUsageException("You must be in Skyblock to use this command!")
77+
val item = mc.thePlayer?.heldItem ?: throw WrongUsageException("You must hold a leather armor piece to use this command")
78+
if ((item.item as? ItemArmor)?.armorMaterial != ItemArmor.ArmorMaterial.LEATHER) throw WrongUsageException("You must hold a leather armor piece to use this command")
79+
val extraAttributes = ItemUtil.getExtraAttributes(item)
80+
if (extraAttributes == null || !extraAttributes.hasKey("uuid")) throw WrongUsageException("This item does not have a UUID!")
81+
val uuid = extraAttributes.getString("uuid")
82+
83+
return item to uuid
7484
}
7585
}

src/main/kotlin/gg/skytils/skytilsmod/commands/impl/CalcXPCommand.kt

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,27 @@ package gg.skytils.skytilsmod.commands.impl
2020

2121
import gg.essential.universal.UChat
2222
import gg.skytils.skytilsmod.Skytils.Companion.failPrefix
23-
import gg.skytils.skytilsmod.Skytils.Companion.prefix
2423
import gg.skytils.skytilsmod.Skytils.Companion.successPrefix
25-
import gg.skytils.skytilsmod.commands.BaseCommand
24+
import gg.skytils.skytilsmod.commands.SkytilsCommandSender
2625
import gg.skytils.skytilsmod.utils.NumberUtil
2726
import gg.skytils.skytilsmod.utils.SkillUtils
28-
import net.minecraft.client.entity.EntityPlayerSP
27+
import org.incendo.cloud.annotations.Argument
28+
import org.incendo.cloud.annotations.Command
29+
import org.incendo.cloud.annotations.Commands
30+
import org.incendo.cloud.annotations.suggestion.Suggestions
31+
import org.incendo.cloud.context.CommandContext
2932

30-
object CalcXPCommand : BaseCommand("skytilscalcxp", aliases = listOf("stcalcxp")) {
31-
override fun getCommandUsage(player: EntityPlayerSP): String =
32-
"/skytilscalcxp (dungeons/skill/zombie_slayer/spider_slayer/wolf_slayer/enderman_slayer) (start level) (end level)"
33-
34-
override fun processCommand(player: EntityPlayerSP, args: Array<String>) {
35-
if (args.size != 3) {
36-
UChat.chat("$prefix §b" + getCommandUsage(player))
37-
return
38-
}
39-
val type = args[0].lowercase()
33+
@Commands
34+
object CalcXPCommand {
35+
@Command("calcxp <type> <start> <end>")
36+
fun calcXP(
37+
@Argument("type", description = "The type of xp", suggestions = "calcxp_types")
38+
type: String,
39+
@Argument("start", description = "The starting level")
40+
start: Double,
41+
@Argument("end", description = "The ending level")
42+
end: Double
43+
) {
4044
val xpMap = when {
4145
type.endsWith("_slayer") ->
4246
SkillUtils.slayerXp[type.substringBefore("_slayer")] ?: run {
@@ -50,8 +54,8 @@ object CalcXPCommand : BaseCommand("skytilscalcxp", aliases = listOf("stcalcxp")
5054
return
5155
}
5256
}
53-
val starting = args[1].toDoubleOrNull()?.coerceIn(0.0, xpMap.keys.last().toDouble()) ?: 0.0
54-
val ending = args[2].toDoubleOrNull()?.coerceIn(0.0, xpMap.keys.last().toDouble()) ?: 0.0
57+
val starting = start.coerceIn(0.0, xpMap.keys.last().toDouble())
58+
val ending = end.coerceIn(0.0, xpMap.keys.last().toDouble())
5559
if (ending < starting) {
5660
UChat.chat("$failPrefix §cYour start level must be less than your end level.")
5761
}
@@ -67,4 +71,11 @@ object CalcXPCommand : BaseCommand("skytilscalcxp", aliases = listOf("stcalcxp")
6771
}
6872
UChat.chat("$successPrefix §bYou need §6${NumberUtil.nf.format(sum)} xp§b to get from §6$type§b level §6${starting}§b to level §6$ending§b!")
6973
}
74+
75+
private val validTypes: Set<String> = setOf("dungeons", "skill") + SkillUtils.slayerXp.keys.map { it + "_slayer" }
76+
77+
@Suggestions("calcxp_types")
78+
fun xpTypeSuggestions(ctx: CommandContext<SkytilsCommandSender>, input: String): Iterable<String> {
79+
return validTypes.filter { it.startsWith(input) }
80+
}
7081
}

0 commit comments

Comments
 (0)