From ebcb06df4092500a38e9a1a8d56d249b5ff37c47 Mon Sep 17 00:00:00 2001
From: Linnea Gräf <nea@nea.moe>
Date: Sun, 22 Dec 2024 21:17:11 +0100
Subject: feat: Add party commands

---
 .../kotlin/events/PartyMessageReceivedEvent.kt     |   9 ++
 src/main/kotlin/features/chat/PartyCommands.kt     | 134 +++++++++++++++++++++
 src/main/kotlin/util/MC.kt                         |   5 +-
 3 files changed, 147 insertions(+), 1 deletion(-)
 create mode 100644 src/main/kotlin/events/PartyMessageReceivedEvent.kt
 create mode 100644 src/main/kotlin/features/chat/PartyCommands.kt

(limited to 'src/main')

diff --git a/src/main/kotlin/events/PartyMessageReceivedEvent.kt b/src/main/kotlin/events/PartyMessageReceivedEvent.kt
new file mode 100644
index 0000000..4688dfe
--- /dev/null
+++ b/src/main/kotlin/events/PartyMessageReceivedEvent.kt
@@ -0,0 +1,9 @@
+package moe.nea.firmament.events
+
+data class PartyMessageReceivedEvent(
+	val from: ProcessChatEvent,
+	val message: String,
+	val name: String,
+) : FirmamentEvent() {
+	companion object : FirmamentEventBus<PartyMessageReceivedEvent>()
+}
diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt
new file mode 100644
index 0000000..de3a0d9
--- /dev/null
+++ b/src/main/kotlin/features/chat/PartyCommands.kt
@@ -0,0 +1,134 @@
+package moe.nea.firmament.features.chat
+
+import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.exceptions.CommandSyntaxException
+import com.mojang.brigadier.tree.LiteralCommandNode
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.PartyMessageReceivedEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.useMatch
+
+object PartyCommands {
+
+	val messageInChannel = "(?<channel>Party|Guild) >([^:]+?)? (?<name>[^: ]+): (?<message>.+)".toPattern()
+
+	@Subscribe
+	fun onChat(event: ProcessChatEvent) {
+		messageInChannel.useMatch(event.unformattedString) {
+			val channel = group("channel")
+			val message = group("message")
+			val name = group("name")
+			if (channel == "Party") {
+				PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent(
+					event, message, name
+				))
+			}
+		}
+	}
+
+	val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet()
+
+	data class PartyCommandContext(
+		val name: String
+	)
+
+	val dispatch = CommandDispatcher<PartyCommandContext>().also { dispatch ->
+		fun register(
+			name: String,
+			vararg alias: String,
+			block: CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>.() -> Unit = {},
+		): LiteralCommandNode<PartyCommandContext> {
+			val node =
+				dispatch.register(CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>(name).also(block))
+			alias.forEach { register(it) { redirect(node) } }
+			return node
+		}
+
+		register("warp", "pw", "pwarp", "partywarp") {
+			executes {
+				// TODO: add check if you are the party leader
+				MC.sendCommand("p warp")
+				0
+			}
+		}
+
+		register("transfer", "pt", "ptme") {
+			executes {
+				MC.sendCommand("p transfer ${it.source.name}")
+				0
+			}
+		}
+
+		register("allinvite", "allinv") {
+			executes {
+				MC.sendCommand("p settings allinvite")
+				0
+			}
+		}
+
+		register("coords") {
+			executes {
+				val p = MC.player?.blockPos ?: BlockPos.ORIGIN
+				MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}")
+				0
+			}
+		}
+		// TODO: downtime tracker (display message again at end of dungeon)
+		// instance ends: kuudra, dungeons, bacte
+		// TODO: at TPS command
+	}
+
+	object TConfig : ManagedConfig("party-commands", Category.CHAT) {
+		val enable by toggle("enable") { false }
+		val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds }
+		val ignoreOwnCommands by toggle("ignore-own") { false }
+	}
+
+	var lastCommand = TimeMark.farPast()
+
+	@Subscribe
+	fun listPartyCommands(event: CommandEvent.SubCommand) {
+		event.subcommand("partycommands") {
+			thenExecute {
+				// TODO: Better help, including descriptions and redirect detection
+				MC.sendChat(tr("firmament.partycommands.help", "Available party commands: ${dispatch.root.children.map { it.name }}. Available prefixes: $commandPrefixes"))
+			}
+		}
+	}
+
+	@Subscribe
+	fun onPartyMessage(event: PartyMessageReceivedEvent) {
+		if (!TConfig.enable) return
+		if (event.message.firstOrNull() !in commandPrefixes) return
+		if (event.name == MC.playerName && TConfig.ignoreOwnCommands) return
+		if (lastCommand.passedTime() < TConfig.cooldown) {
+			MC.sendChat(tr("firmament.partycommands.cooldown", "Skipping party command. Cooldown not passed."))
+			return
+		}
+		// TODO: add trust levels
+		val commandLine = event.message.substring(1)
+		try {
+			dispatch.execute(StringReader(commandLine), PartyCommandContext(event.name))
+		} catch (ex: Exception) {
+			if (ex is CommandSyntaxException) {
+				MC.sendChat(tr("firmament.partycommands.unknowncommand", "Unknown party command."))
+				return
+			} else {
+				MC.sendChat(tr("firmament.partycommands.unknownerror", "Unknown error during command execution."))
+				ErrorUtil.softError("Unknown error during command execution.", ex)
+			}
+		}
+		lastCommand = TimeMark.now()
+	}
+}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
index 294334a..215d2a8 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -64,6 +64,8 @@ object MC {
 	}
 
 	fun sendCommand(command: String) {
+		// TODO: add a queue to this and sendServerChat
+		ErrorUtil.softCheck("Server commands have an implied /", !command.startsWith("/"))
 		player?.networkHandler?.sendCommand(command)
 	}
 
@@ -96,8 +98,9 @@ object MC {
 	inline val camera: Entity? get() = instance.cameraEntity
 	inline val guiAtlasManager get() = instance.guiAtlasManager
 	inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world }
+	inline val playerName: String? get() = player?.name?.unformattedString
 	inline var screen: Screen?
-		get() = TestUtil.unlessTesting{ instance.currentScreen }
+		get() = TestUtil.unlessTesting { instance.currentScreen }
 		set(value) = instance.setScreen(value)
 	val screenName get() = screen?.title?.unformattedString?.trim()
 	inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
-- 
cgit