feat: Add basic combo buttons (without editor for now)

This commit is contained in:
Linnea Gräf
2025-06-04 01:06:51 +02:00
parent 9ad691bc1b
commit 2a1631dadf
6 changed files with 310 additions and 6 deletions

View File

@@ -2,18 +2,19 @@
package moe.nea.firmament.mixins;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.events.WorldKeyboardEvent;
import net.minecraft.client.Keyboard;
import net.minecraft.client.util.InputUtil;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Keyboard.class)
public class KeyPressInWorldEventPatch {
@Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"))
public void onKeyBoardInWorld(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) {
WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(key, scancode, modifiers));
}
@WrapWithCondition(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"))
public boolean onKeyBoardInWorld(InputUtil.Key key, long window, int _key, int scancode, int action, int modifiers) {
var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(_key, scancode, modifiers));
return !event.getCancelled();
}
}

View File

@@ -0,0 +1,104 @@
package moe.nea.firmament.features.macros
import kotlin.time.Duration.Companion.seconds
import net.minecraft.client.util.InputUtil
import net.minecraft.text.Text
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.keybindings.SavedKeyBinding
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
object ComboProcessor {
var rootTrie: Branch = Branch(mapOf())
private set
var activeTrie: Branch = rootTrie
private set
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)
reset()
}
fun reset() {
activeTrie = rootTrie
lastInput = TimeMark.now()
isInputting = false
breadCrumbs.clear()
}
@Subscribe
fun onTick(event: TickEvent) {
if (isInputting && lastInput.passedTime() > 3.seconds)
reset()
}
@Subscribe
fun onRender(event: HudRenderEvent) {
if (!isInputting) return
if (!event.isRenderingHud) return
event.context.matrices.push()
val width = 120
event.context.matrices.translate(
(MC.window.scaledWidth - width) / 2F,
(MC.window.scaledHeight) / 2F + 8,
0F
)
val breadCrumbText = breadCrumbs.joinToString(" > ")
event.context.drawText(MC.font, breadCrumbText, 0, 0, -1, true)
event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F)
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.matrices.pop()
}
@Subscribe
fun onKeyBinding(event: WorldKeyboardEvent) {
val nextEntry = activeTrie.nodes.entries
.find { event.matches(it.key) }
if (nextEntry == null) {
reset()
return
}
event.cancel()
breadCrumbs.add(nextEntry.key)
lastInput = TimeMark.now()
isInputting = true
val value = nextEntry.value
when (value) {
is Branch -> {
activeTrie = value
}
is Leaf -> {
value.execute()
reset()
}
}.let { }
}
}

View File

@@ -0,0 +1,35 @@
package moe.nea.firmament.features.macros
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
interface HotkeyAction {
// TODO: execute
val label: Text
fun execute()
}
data class CommandAction(val command: String) : HotkeyAction {
override val label: Text
get() = Text.literal("/$command")
override fun execute() {
MC.sendCommand(command)
}
}
// Mit onscreen anzeige:
// F -> 1 /equipment
// F -> 2 /wardrobe
// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße)
// Radial menu
// Hold F
// Weight (mach eins doppelt so groß)
// /equipment
// /wardrobe
// Bei allen: Filter!
// - Nur in Dungeons / andere Insel
// - Nur wenn ich Item X im inventar habe (fishing rod)

View File

@@ -0,0 +1,57 @@
package moe.nea.firmament.features.macros
import net.minecraft.text.Text
import moe.nea.firmament.keybindings.SavedKeyBinding
sealed interface KeyComboTrie {
val label: Text
companion object {
fun fromComboList(
combos: List<ComboKeyAction>,
): Branch {
val root = Branch(mutableMapOf())
for (combo in combos) {
var p = root
require(combo.keys.isNotEmpty())
for ((index, key) in combo.keys.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).")
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
}
}
}
return root
}
}
}
data class ComboKeyAction(
val action: HotkeyAction,
val keys: List<SavedKeyBinding>,
)
data class Leaf(val action: HotkeyAction) : KeyComboTrie {
override val label: Text
get() = action.label
fun execute() {
action.execute()
}
}
data class Branch(
val nodes: Map<SavedKeyBinding, KeyComboTrie>
) : KeyComboTrie {
override val label: Text
get() = Text.literal("...") // TODO: better labels
}

View File

@@ -87,6 +87,10 @@ data class SavedKeyBinding(
return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt)
}
override fun toString(): String {
return format().string
}
fun format(): Text {
val stroke = Text.literal("")
if (ctrl) {

View File

@@ -0,0 +1,103 @@
package moe.nea.firmament.test.features.macros
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import net.minecraft.client.util.InputUtil
import moe.nea.firmament.features.macros.Branch
import moe.nea.firmament.features.macros.ComboKeyAction
import moe.nea.firmament.features.macros.CommandAction
import moe.nea.firmament.features.macros.KeyComboTrie
import moe.nea.firmament.features.macros.Leaf
import moe.nea.firmament.keybindings.SavedKeyBinding
class KeyComboTrieCreation {
val basicAction = CommandAction("ac Hello")
val aPress = SavedKeyBinding(InputUtil.GLFW_KEY_A)
val bPress = SavedKeyBinding(InputUtil.GLFW_KEY_B)
val cPress = SavedKeyBinding(InputUtil.GLFW_KEY_C)
@Test
fun testValidShortTrie() {
val actions = listOf(
ComboKeyAction(basicAction, listOf(aPress)),
ComboKeyAction(basicAction, listOf(bPress)),
ComboKeyAction(basicAction, listOf(cPress)),
)
Assertions.assertEquals(
Branch(
mapOf(
aPress to Leaf(basicAction),
bPress to Leaf(basicAction),
cPress to Leaf(basicAction),
),
), KeyComboTrie.fromComboList(actions)
)
}
@Test
fun testOverlappingLeafs() {
Assertions.assertThrows(IllegalStateException::class.java) {
KeyComboTrie.fromComboList(
listOf(
ComboKeyAction(basicAction, listOf(aPress, aPress)),
ComboKeyAction(basicAction, listOf(aPress, aPress)),
)
)
}
Assertions.assertThrows(IllegalStateException::class.java) {
KeyComboTrie.fromComboList(
listOf(
ComboKeyAction(basicAction, listOf(aPress)),
ComboKeyAction(basicAction, listOf(aPress)),
)
)
}
}
@Test
fun testBranchOverlappingLeaf() {
Assertions.assertThrows(IllegalStateException::class.java) {
KeyComboTrie.fromComboList(
listOf(
ComboKeyAction(basicAction, listOf(aPress)),
ComboKeyAction(basicAction, listOf(aPress, aPress)),
)
)
}
}
@Test
fun testLeafOverlappingBranch() {
Assertions.assertThrows(IllegalStateException::class.java) {
KeyComboTrie.fromComboList(
listOf(
ComboKeyAction(basicAction, listOf(aPress, aPress)),
ComboKeyAction(basicAction, listOf(aPress)),
)
)
}
}
@Test
fun testValidNestedTrie() {
val actions = listOf(
ComboKeyAction(basicAction, listOf(aPress, aPress)),
ComboKeyAction(basicAction, listOf(aPress, bPress)),
ComboKeyAction(basicAction, listOf(cPress)),
)
Assertions.assertEquals(
Branch(
mapOf(
aPress to Branch(
mapOf(
aPress to Leaf(basicAction),
bPress to Leaf(basicAction),
)
),
cPress to Leaf(basicAction),
),
), KeyComboTrie.fromComboList(actions)
)
}
}