diff options
Diffstat (limited to 'src/main/kotlin/features/macros')
| -rw-r--r-- | src/main/kotlin/features/macros/ComboProcessor.kt | 51 | ||||
| -rw-r--r-- | src/main/kotlin/features/macros/HotkeyAction.kt | 15 | ||||
| -rw-r--r-- | src/main/kotlin/features/macros/KeyComboTrie.kt | 44 | ||||
| -rw-r--r-- | src/main/kotlin/features/macros/MacroData.kt | 19 | ||||
| -rw-r--r-- | src/main/kotlin/features/macros/MacroUI.kt | 293 | ||||
| -rw-r--r-- | src/main/kotlin/features/macros/RadialMenu.kt | 153 |
6 files changed, 530 insertions, 45 deletions
diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt index 55b3f6e..9dadb80 100644 --- a/src/main/kotlin/features/macros/ComboProcessor.kt +++ b/src/main/kotlin/features/macros/ComboProcessor.kt @@ -1,8 +1,7 @@ package moe.nea.firmament.features.macros import kotlin.time.Duration.Companion.seconds -import net.minecraft.client.util.InputUtil -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.TickEvent @@ -10,6 +9,7 @@ import moe.nea.firmament.events.WorldKeyboardEvent import moe.nea.firmament.keybindings.SavedKeyBinding import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr object ComboProcessor { @@ -22,20 +22,6 @@ object ComboProcessor { var isInputting = false var lastInput = TimeMark.farPast() val breadCrumbs = mutableListOf<SavedKeyBinding>() - // TODO: keep breadcrumbs - - - init { - val f = SavedKeyBinding(InputUtil.GLFW_KEY_F) - val one = SavedKeyBinding(InputUtil.GLFW_KEY_1) - val two = SavedKeyBinding(InputUtil.GLFW_KEY_2) - setActions( - listOf( - ComboKeyAction(CommandAction("wardrobe"), listOf(f, one)), - ComboKeyAction(CommandAction("equipment"), listOf(f, two)), - ) - ) - } fun setActions(actions: List<ComboKeyAction>) { rootTrie = KeyComboTrie.fromComboList(actions) @@ -60,21 +46,34 @@ object ComboProcessor { fun onRender(event: HudRenderEvent) { if (!isInputting) return if (!event.isRenderingHud) return - event.context.matrices.push() + event.context.pose().pushMatrix() val width = 120 - event.context.matrices.translate( - (MC.window.scaledWidth - width) / 2F, - (MC.window.scaledHeight) / 2F + 8, - 0F + event.context.pose().translate( + (MC.window.guiScaledWidth - width) / 2F, + (MC.window.guiScaledHeight) / 2F + 8 ) val breadCrumbText = breadCrumbs.joinToString(" > ") - event.context.drawText(MC.font, breadCrumbText, 0, 0, -1, true) - event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F) + event.context.drawString( + MC.font, + tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText), + 0, + 0, + -1, + true + ) + event.context.pose().translate(0F, MC.font.lineHeight + 2F) for ((key, value) in activeTrie.nodes) { - event.context.drawText(MC.font, Text.literal("$breadCrumbText > $key: ").append(value.label), 0, 0, -1, true) - event.context.matrices.translate(0F, MC.font.fontHeight + 1F, 0F) + event.context.drawString( + MC.font, + Component.literal("$breadCrumbText > $key: ").append(value.label), + 0, + 0, + -1, + true + ) + event.context.pose().translate(0F, MC.font.lineHeight + 1F) } - event.context.matrices.pop() + event.context.pose().popMatrix() } @Subscribe diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt index 51c1baa..18c95bc 100644 --- a/src/main/kotlin/features/macros/HotkeyAction.kt +++ b/src/main/kotlin/features/macros/HotkeyAction.kt @@ -1,17 +1,22 @@ package moe.nea.firmament.features.macros -import net.minecraft.text.Text +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.minecraft.network.chat.Component import moe.nea.firmament.util.MC -interface HotkeyAction { +@Serializable +sealed interface HotkeyAction { // TODO: execute - val label: Text + val label: Component fun execute() } +@Serializable +@SerialName("command") data class CommandAction(val command: String) : HotkeyAction { - override val label: Text - get() = Text.literal("/$command") + override val label: Component + get() = Component.literal("/$command") override fun execute() { MC.sendCommand(command) diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt index 5c14bcd..2701ac1 100644 --- a/src/main/kotlin/features/macros/KeyComboTrie.kt +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -1,10 +1,12 @@ package moe.nea.firmament.features.macros -import net.minecraft.text.Text +import kotlinx.serialization.Serializable +import net.minecraft.network.chat.Component import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil sealed interface KeyComboTrie { - val label: Text + val label: Component companion object { fun fromComboList( @@ -13,19 +15,27 @@ sealed interface KeyComboTrie { val root = Branch(mutableMapOf()) for (combo in combos) { var p = root - require(combo.keys.isNotEmpty()) - for ((index, key) in combo.keys.withIndex()) { + if (combo.keySequence.isEmpty()) { + ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty") + continue + } + for ((index, key) in combo.keySequence.withIndex()) { val m = (p.nodes as MutableMap) - if (index == combo.keys.lastIndex) { - if (key in m) - error("Overlapping actions found for ${combo.keys} (another action ${m[key]} already exists).") + if (index == combo.keySequence.lastIndex) { + if (key in m) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence.joinToString(" > ")} (another action ${m[key]} already exists).") + break + } m[key] = Leaf(combo.action) } else { val c = m.getOrPut(key) { Branch(mutableMapOf()) } - if (c !is Branch) - error("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already") - p = c + if (c !is Branch) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence} (final node exists at index $index) through another action already") + break + } else { + p = c + } } } } @@ -34,14 +44,20 @@ sealed interface KeyComboTrie { } } +@Serializable +data class MacroWheel( + val keyBinding: SavedKeyBinding = SavedKeyBinding.unbound(), + val options: List<HotkeyAction> +) +@Serializable data class ComboKeyAction( val action: HotkeyAction, - val keys: List<SavedKeyBinding>, + val keySequence: List<SavedKeyBinding> = listOf(), ) data class Leaf(val action: HotkeyAction) : KeyComboTrie { - override val label: Text + override val label: Component get() = action.label fun execute() { @@ -52,6 +68,6 @@ data class Leaf(val action: HotkeyAction) : KeyComboTrie { data class Branch( val nodes: Map<SavedKeyBinding, KeyComboTrie> ) : KeyComboTrie { - override val label: Text - get() = Text.literal("...") // TODO: better labels + override val label: Component + get() = Component.literal("...") // TODO: better labels } diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt new file mode 100644 index 0000000..af1b0e8 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.DataHolder + +@Serializable +data class MacroData( + var comboActions: List<ComboKeyAction> = listOf(), + var wheels: List<MacroWheel> = listOf(), +) { + @Config + object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) { + override fun onLoad() { + ComboProcessor.setActions(data.comboActions) + RadialMacros.setWheels(data.wheels) + } + } +} diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt new file mode 100644 index 0000000..e73f076 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -0,0 +1,293 @@ +package moe.nea.firmament.features.macros + +import io.github.notenoughupdates.moulconfig.common.text.StructuredText +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList +import moe.nea.firmament.gui.config.KeyBindingStateManager +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil + +class MacroUI { + + + companion object { + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + // TODO: add button in config + event.subcommand("macros") { + thenExecute { + ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null)) + } + } + } + + } + + @field:Bind("combos") + val combos = Combos() + + @field:Bind("wheels") + val wheels = Wheels() + var dontSave = false + + @Bind + fun beforeClose(): CloseEventListener.CloseAction { + if (!dontSave) + save() + return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE + } + + fun save() { + MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() } + MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() } + MacroData.DConfig.markDirty() + RadialMacros.setWheels(MacroData.DConfig.data.wheels) + ComboProcessor.setActions(MacroData.DConfig.data.comboActions) + } + + fun discard() { + dontSave = true + MC.screen?.onClose() + } + + class Command( + @field:Bind("text") + var text: String, + val parent: Wheel, + ) { + @Bind + fun textR() = StructuredText.of(text) + + @Bind + fun delete() { + parent.editableCommands.removeIf { it === this } + parent.editableCommands.update() + parent.commands.update() + } + + fun asCommandAction() = CommandAction(text) + } + + inner class Wheel( + val parent: Wheels, + var binding: SavedKeyBinding, + commands: List<CommandAction>, + ) { + + fun asSaveable(): MacroWheel { + return MacroWheel(binding, commands.map { it.asCommandAction() }) + } + + @Bind("keyCombo") + fun text() = MoulConfigPlatform.wrap(binding.format()) + + @field:Bind("commands") + val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) } + + @field:Bind("editableCommands") + val editableCommands = this.commands.toObservableList() + + @Bind + fun addOption() { + editableCommands.add(Command("", this)) + } + + @Bind + fun back() { + MC.screen?.onClose() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen) + } + + @Bind + fun delete() { + parent.wheels.removeIf { it === this } + parent.wheels.update() + } + + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + + fun requestFocus() { + button.requestFocus() + } + } + + inner class Wheels { + @field:Bind("wheels") + val wheels: ObservableList<Wheel> = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) { + Wheel(this, it.keyBinding, it.options.map { CommandAction((it as CommandAction).command) }) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + + @Bind + fun addWheel() { + wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf())) + } + } + + fun saveAndClose() { + save() + MC.screen?.onClose() + } + + inner class Combos { + @field:Bind("actions") + val actions: ObservableList<ActionEditor> = ObservableList( + MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { + ActionEditor(it, this) + } + ) + + @Bind + fun addCommand() { + actions.add( + ActionEditor( + ComboKeyAction( + CommandAction("ac Hello from a Firmament Hotkey"), + listOf() + ), + this + ) + ) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + } + + class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) { + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + + fun requestFocus() { + button.requestFocus() + } + + @Bind + fun delete() { + parent.combo.removeIf { it === this } + parent.combo.update() + } + } + + class ActionEditor(val action: ComboKeyAction, val parent: Combos) { + fun asSaveable(): ComboKeyAction { + return ComboKeyAction( + CommandAction(command), + combo.map { it.binding } + ) + } + + @field:Bind("command") + var command: String = (action.action as CommandAction).command + + @Bind + fun commandR() = StructuredText.of(command) + + @field:Bind("combo") + val combo = action.keySequence.map { KeyBindingEditor(it, this) }.toObservableList() + + @Bind + fun formattedCombo() = + StructuredText.of(combo.joinToString(" > ") { it.binding.toString() }) // TODO: this can be joined without .toString() + + @Bind + fun addStep() { + combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this)) + } + + @Bind + fun back() { + MC.screen?.onClose() + } + + @Bind + fun delete() { + parent.actions.removeIf { it === this } + parent.actions.update() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen) + } + } +} + +private fun <T> ObservableList<T>.setAll(ts: Collection<T>) { + val observer = this.observer + this.clear() + this.addAll(ts) + this.observer = observer + this.update() +} diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt new file mode 100644 index 0000000..2519123 --- /dev/null +++ b/src/main/kotlin/features/macros/RadialMenu.kt @@ -0,0 +1,153 @@ +package moe.nea.firmament.features.macros + +import me.shedaniel.math.Color +import org.joml.Vector2f +import util.render.CustomRenderLayers +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt +import net.minecraft.client.gui.GuiGraphics +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldMouseMoveEvent +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderCircleProgress +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.render.lerpAngle +import moe.nea.firmament.util.render.wrapAngle +import moe.nea.firmament.util.render.τ + +object RadialMenuViewer { + interface RadialMenu { + val key: SavedKeyBinding + val options: List<RadialMenuOption> + } + + interface RadialMenuOption { + val isEnabled: Boolean + fun resolve() + fun renderSlice(drawContext: GuiGraphics) + } + + var activeMenu: RadialMenu? = null + set(value) { + if (value?.options.isNullOrEmpty()) { + field = null + } else { + field = value + } + delta = Vector2f(0F, 0F) + } + var delta = Vector2f(0F, 0F) + val maxSelectionSize = 100F + + @Subscribe + fun onMouseMotion(event: WorldMouseMoveEvent) { + val menu = activeMenu ?: return + event.cancel() + delta.add(event.deltaX.toFloat(), event.deltaY.toFloat()) + val m = delta.lengthSquared() + if (m > maxSelectionSize * maxSelectionSize) { + delta.mul(maxSelectionSize / sqrt(m)) + } + } + + val INNER_CIRCLE_RADIUS = 16 + + @Subscribe + fun onRender(event: HudRenderEvent) { + val menu = activeMenu ?: return + val mat = event.context.pose() + mat.pushMatrix() + mat.translate( + (MC.window.guiScaledWidth) / 2F, + (MC.window.guiScaledHeight) / 2F, + ) + val sliceWidth = (τ / menu.options.size).toFloat() + var selectedAngle = wrapAngle(atan2(delta.y, delta.x)) + if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS) + selectedAngle = Float.NaN + for ((idx, option) in menu.options.withIndex()) { + val range = (sliceWidth * idx)..(sliceWidth * (idx + 1)) + mat.pushMatrix() + mat.scale(64F, 64F) + val cutout = INNER_CIRCLE_RADIUS / 64F / 2 + RenderCircleProgress.renderCircularSlice( + event.context, + CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI, + 0F, 1F, 0F, 1F, + range, + color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF, + innerCutoutRadius = cutout + ) + mat.popMatrix() + mat.pushMatrix() + val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F) + val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F) + mat.translate(vec.x, vec.y) + option.renderSlice(event.context) + mat.popMatrix() + } + event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), Color.ofOpaque(0x00FF00)) + mat.popMatrix() + } + + @Subscribe + fun onTick(event: TickEvent) { + val menu = activeMenu ?: return + if (!menu.key.isPressed(true)) { + val angle = atan2(delta.y, delta.x) + + val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt() + val choice = menu.options[choiceIndex] + val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS + activeMenu = null + if (selectedAny) + choice.resolve() + } + } + +} + +object RadialMacros { + lateinit var wheels: List<MacroWheel> + private set + + fun setWheels(wheels: List<MacroWheel>) { + this.wheels = wheels + RadialMenuViewer.activeMenu = null + } + + @Subscribe + fun onOpen(event: WorldKeyboardEvent) { + if (RadialMenuViewer.activeMenu != null) return + wheels.forEach { wheel -> + if (event.matches(wheel.keyBinding, atLeast = true)) { + class R(val action: HotkeyAction) : RadialMenuOption { + override val isEnabled: Boolean + get() = true + + override fun resolve() { + action.execute() + } + + override fun renderSlice(drawContext: GuiGraphics) { + drawContext.drawCenteredString(MC.font, action.label, 0, 0, -1) + } + } + RadialMenuViewer.activeMenu = object : RadialMenu { + override val key: SavedKeyBinding + get() = wheel.keyBinding + override val options: List<RadialMenuOption> = + wheel.options.map { R(it) } + } + } + } + } +} |
