diff options
author | Linnea Gräf <nea@nea.moe> | 2025-06-17 20:59:45 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2025-06-17 21:19:43 +0200 |
commit | 4b9e966ca7e8a9291f307850f715820e122d69fd (patch) | |
tree | 94b08c865c882f93ff9c0c6aa7a507d47bcc67bf /src/main/kotlin/features | |
parent | 775933d516db10dbcc1cbbe405defa7b6e0c5a6a (diff) | |
download | Firmament-4b9e966ca7e8a9291f307850f715820e122d69fd.tar.gz Firmament-4b9e966ca7e8a9291f307850f715820e122d69fd.tar.bz2 Firmament-4b9e966ca7e8a9291f307850f715820e122d69fd.zip |
feat: Add macro wheels
Diffstat (limited to 'src/main/kotlin/features')
-rw-r--r-- | src/main/kotlin/features/macros/KeyComboTrie.kt | 5 | ||||
-rw-r--r-- | src/main/kotlin/features/macros/MacroData.kt | 5 | ||||
-rw-r--r-- | src/main/kotlin/features/macros/MacroUI.kt | 170 | ||||
-rw-r--r-- | src/main/kotlin/features/macros/RadialMenu.kt | 149 |
4 files changed, 304 insertions, 25 deletions
diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt index 57ff289..452bc56 100644 --- a/src/main/kotlin/features/macros/KeyComboTrie.kt +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -44,6 +44,11 @@ sealed interface KeyComboTrie { } } +@Serializable +data class MacroWheel( + val key: SavedKeyBinding, + val options: List<HotkeyAction> +) @Serializable data class ComboKeyAction( diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt index 78a5948..91de423 100644 --- a/src/main/kotlin/features/macros/MacroData.kt +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -5,7 +5,8 @@ import moe.nea.firmament.util.data.DataHolder @Serializable data class MacroData( - var comboActions: List<ComboKeyAction> = listOf(), -){ + var comboActions: List<ComboKeyAction> = listOf(), + var wheels: List<MacroWheel> = listOf(), +) { object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) } diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt index 17fdd0a..88f20a1 100644 --- a/src/main/kotlin/features/macros/MacroUI.kt +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -32,7 +32,142 @@ class MacroUI { @field:Bind("combos") val combos = Combos() - class 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?.close() + } + + class Command( + @field:Bind("text") + var text: String, + val parent: Wheel, + ) { + @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() = binding.format().string + + @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?.close() + } + + @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.key, 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?.close() + } + + inner class Combos { @field:Bind("actions") val actions: ObservableList<ActionEditor> = ObservableList( MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { @@ -40,15 +175,6 @@ class MacroUI { } ) - var dontSave = false - - @Bind - fun beforeClose(): CloseEventListener.CloseAction { - if (!dontSave) - save() - return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE - } - @Bind fun addCommand() { actions.add( @@ -64,21 +190,17 @@ class MacroUI { @Bind fun discard() { - dontSave = true - MC.screen?.close() + this@MacroUI.discard() } @Bind fun saveAndClose() { - save() - MC.screen?.close() + this@MacroUI.discard() } @Bind fun save() { - MacroData.DConfig.data.comboActions = actions.map { it.asSaveable() } - MacroData.DConfig.markDirty() - ComboProcessor.setActions(MacroData.DConfig.data.comboActions) // TODO: automatically reload those from the config on startup + this@MacroUI.save() } } @@ -101,18 +223,19 @@ class MacroUI { button.blur() } + + fun requestFocus() { + button.requestFocus() + } + @Bind fun delete() { parent.combo.removeIf { it === this } parent.combo.update() } - - fun requestFocus() { - button.requestFocus() - } } - class ActionEditor(val action: ComboKeyAction, val parent: MacroUI.Combos) { + class ActionEditor(val action: ComboKeyAction, val parent: Combos) { fun asSaveable(): ComboKeyAction { return ComboKeyAction( CommandAction(command), @@ -145,9 +268,10 @@ class MacroUI { parent.actions.removeIf { it === this } parent.actions.update() } + @Bind fun edit() { - MC.screen = MoulConfigUtils.loadScreen("config/macros/editor", this, MC.screen) + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen) } } } diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt new file mode 100644 index 0000000..2e09c44 --- /dev/null +++ b/src/main/kotlin/features/macros/RadialMenu.kt @@ -0,0 +1,149 @@ +package moe.nea.firmament.features.macros + +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.DrawContext +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: DrawContext) + } + + var activeMenu: RadialMenu? = null + set(value) { + 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.matrices + mat.push() + mat.translate( + (MC.window.scaledWidth) / 2F, + (MC.window.scaledHeight) / 2F, + 0F + ) + 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.push() + mat.scale(64F, 64F, 1F) + 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.pop() + mat.push() + val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F) + val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F) + mat.translate(vec.x, vec.y, 0F) + option.renderSlice(event.context) + mat.pop() + } + event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), me.shedaniel.math.Color.ofOpaque(0x00FF00)) + mat.pop() + } + + @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 { + var wheels = MacroData.DConfig.data.wheels + 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.key, atLeast = true)) { + class R(val action: HotkeyAction) : RadialMenuOption { + override val isEnabled: Boolean + get() = true + + override fun resolve() { + action.execute() + } + + override fun renderSlice(drawContext: DrawContext) { + drawContext.drawCenteredTextWithShadow(MC.font, action.label, 0, 0, -1) + } + } + RadialMenuViewer.activeMenu = object : RadialMenu { + override val key: SavedKeyBinding + get() = wheel.key + override val options: List<RadialMenuOption> = + wheel.options.map { R(it) } + } + } + } + } +} |