Add slot binding

This commit is contained in:
Linnea Gräf
2024-10-18 00:41:25 +02:00
parent 7de0e8e7e0
commit c89b663aca
9 changed files with 609 additions and 307 deletions

View File

@@ -2,8 +2,8 @@
package moe.nea.firmament.init;
import me.shedaniel.mm.api.ClassTinkerers;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.client.gui.Element;
import net.minecraft.client.gui.ParentElement;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
@@ -17,6 +17,7 @@ import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import java.lang.reflect.Modifier;
import java.util.function.Consumer;
public class HandledScreenRiser extends RiserUtils {
@IntermediaryName(net.minecraft.client.gui.screen.Screen.class)
@@ -26,13 +27,89 @@ public class HandledScreenRiser extends RiserUtils {
Type mouseScrolledDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE);
String mouseScrolled = remapper.mapMethodName("intermediary", "net.minecraft.class_364", "method_25401",
mouseScrolledDesc.getDescriptor());
// boolean keyReleased(int keyCode, int scanCode, int modifiers)
Type keyReleasedDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.INT_TYPE, Type.INT_TYPE, Type.INT_TYPE);
String keyReleased = remapper.mapMethodName("intermediary", Intermediary.<Element>className(),
Intermediary.methodName(Element::keyReleased),
keyReleasedDesc.getDescriptor());
@Override
public void addTinkerers() {
ClassTinkerers.addTransformation(HandledScreen, this::handle, true);
ClassTinkerers.addTransformation(HandledScreen, this::addMouseScroll, true);
ClassTinkerers.addTransformation(HandledScreen, this::addKeyReleased, true);
}
void handle(ClassNode classNode) {
/**
* Insert a handler that roughly inserts the following code at the beginning of the instruction list:
* <code><pre>
* if (insertInvoke(insertLoads)) return true
* </pre></code>
*
* @param node The method node to prepend the instructions to
* @param insertLoads insert all the loads, including the {@code this} parameter
* @param insertInvoke insert the invokevirtual/invokestatic call
*/
void insertTrueHandler(MethodNode node,
Consumer<InsnList> insertLoads,
Consumer<InsnList> insertInvoke) {
var insns = new InsnList();
insertLoads.accept(insns);
insertInvoke.accept(insns);
// Create jump target (but not insert it yet)
var jumpIfFalse = new LabelNode();
// IFEQ (if returned boolean == 0), jump to jumpIfFalse
insns.add(new JumpInsnNode(Opcodes.IFEQ, jumpIfFalse));
// LDC 1 (as int, which is what booleans are at runtime)
insns.add(new LdcInsnNode(1));
// IRETURN return int on stack (booleans are int at runtime)
insns.add(new InsnNode(Opcodes.IRETURN));
insns.add(jumpIfFalse);
node.instructions.insert(insns);
}
void addKeyReleased(ClassNode classNode) {
var keyReleasedNode = findMethod(classNode, keyReleased, keyReleasedDesc);
if (keyReleasedNode == null) {
keyReleasedNode = new MethodNode(
Modifier.PUBLIC,
keyReleased,
keyReleasedDesc.getDescriptor(),
null,
new String[0]
);
var insns = keyReleasedNode.instructions;
// ALOAD 0, load this
insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
// ILOAD 1-3, load args
insns.add(new VarInsnNode(Opcodes.ILOAD, 1));
insns.add(new VarInsnNode(Opcodes.ILOAD, 2));
insns.add(new VarInsnNode(Opcodes.ILOAD, 3));
// INVOKESPECIAL call super method
insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, getTypeForClassName(Screen).getInternalName(),
keyReleased, keyReleasedDesc.getDescriptor()));
// IRETURN return int on stack (booleans are int at runtime)
insns.add(new InsnNode(Opcodes.IRETURN));
classNode.methods.add(keyReleasedNode);
}
insertTrueHandler(keyReleasedNode, insns -> {
// ALOAD 0, load this
insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
// ILOAD 1-3, load args
insns.add(new VarInsnNode(Opcodes.ILOAD, 1));
insns.add(new VarInsnNode(Opcodes.ILOAD, 2));
insns.add(new VarInsnNode(Opcodes.ILOAD, 3));
}, insns -> {
// INVOKEVIRTUAL call custom handler
insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
getTypeForClassName(HandledScreen).getInternalName(),
"keyReleased_firmament",
keyReleasedDesc.getDescriptor()));
});
}
void addMouseScroll(ClassNode classNode) {
MethodNode mouseScrolledNode = findMethod(classNode, mouseScrolled, mouseScrolledDesc);
if (mouseScrolledNode == null) {
mouseScrolledNode = new MethodNode(
@@ -57,7 +134,7 @@ public class HandledScreenRiser extends RiserUtils {
classNode.methods.add(mouseScrolledNode);
}
var insns = new InsnList();
insertTrueHandler(mouseScrolledNode, insns -> {
// ALOAD 0, load this
insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
// DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time.
@@ -65,21 +142,14 @@ public class HandledScreenRiser extends RiserUtils {
insns.add(new VarInsnNode(Opcodes.DLOAD, 3));
insns.add(new VarInsnNode(Opcodes.DLOAD, 5));
insns.add(new VarInsnNode(Opcodes.DLOAD, 7));
}, insns -> {
// INVOKEVIRTUAL call custom handler
insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
getTypeForClassName(HandledScreen).getInternalName(),
"mouseScrolled_firmament",
mouseScrolledDesc.getDescriptor()));
// Create jump target (but not insert it yet)
var jumpIfFalse = new LabelNode();
// IFEQ (if returned boolean == 0), jump to jumpIfFalse
insns.add(new JumpInsnNode(Opcodes.IFEQ, jumpIfFalse));
// LDC 1 (as int, which is what booleans are at runtime)
insns.add(new LdcInsnNode(1));
// IRETURN return int on stack (booleans are int at runtime)
insns.add(new InsnNode(Opcodes.IRETURN));
insns.add(jumpIfFalse);
mouseScrolledNode.instructions.insert(insns);
});
}
}

View File

@@ -57,6 +57,10 @@ public abstract class MixinHandledScreen<T extends ScreenHandler> {
}
}
boolean keyReleased_firmament(int keyCode, int scanCode, int modifiers) {
return HandledScreenKeyReleasedEvent.Companion.publish(new HandledScreenKeyReleasedEvent((HandledScreen<?>) (Object) this, keyCode, scanCode, modifiers)).getCancelled();
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawForeground(Lnet/minecraft/client/gui/DrawContext;II)V", shift = At.Shift.AFTER))
public void onAfterRenderForeground(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
context.getMatrices().push();

View File

@@ -1,18 +1,14 @@
package moe.nea.firmament.events
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.option.KeyBinding
import moe.nea.firmament.keybindings.IKeyBinding
data class HandledScreenKeyPressedEvent(
val screen: HandledScreen<*>,
val keyCode: Int,
val scanCode: Int,
sealed interface HandledScreenKeyEvent {
val screen: HandledScreen<*>
val keyCode: Int
val scanCode: Int
val modifiers: Int
) : FirmamentEvent.Cancellable() {
companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>()
fun matches(keyBinding: KeyBinding): Boolean {
return matches(IKeyBinding.minecraft(keyBinding))
@@ -22,3 +18,21 @@ data class HandledScreenKeyPressedEvent(
return keyBinding.matches(keyCode, scanCode, modifiers)
}
}
data class HandledScreenKeyPressedEvent(
override val screen: HandledScreen<*>,
override val keyCode: Int,
override val scanCode: Int,
override val modifiers: Int
) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent {
companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>()
}
data class HandledScreenKeyReleasedEvent(
override val screen: HandledScreen<*>,
override val keyCode: Int,
override val scanCode: Int,
override val modifiers: Int
) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent {
companion object : FirmamentEventBus<HandledScreenKeyReleasedEvent>()
}

View File

@@ -1,5 +1,3 @@
@file:UseSerializers(DashlessUUIDSerializer::class)
package moe.nea.firmament.features.inventory
@@ -13,11 +11,15 @@ import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.HandledScreenKeyReleasedEvent
import moe.nea.firmament.events.IsSlotProtectedEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
@@ -28,9 +30,12 @@ import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.render.drawLine
import moe.nea.firmament.util.skyblockUUID
import moe.nea.firmament.util.unformattedString
@@ -42,8 +47,8 @@ object SlotLocking : FirmamentFeature {
data class Data(
val lockedSlots: MutableSet<Int> = mutableSetOf(),
val lockedSlotsRift: MutableSet<Int> = mutableSetOf(),
val lockedUUIDs: MutableSet<UUID> = mutableSetOf(),
val boundSlots: MutableMap<Int, Int> = mutableMapOf()
)
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
@@ -51,6 +56,8 @@ object SlotLocking : FirmamentFeature {
val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") {
SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true)
}
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
}
override val config: TConfig
@@ -136,6 +143,24 @@ object SlotLocking : FirmamentFeature {
}
}
@Subscribe
fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) {
val boundSlots = DConfig.data?.boundSlots ?: mapOf()
val isValidAction =
it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift)
if (!isValidAction) return
val handler = MC.handledScreen?.screenHandler ?: return
val slot = it.slot
if (slot != null && it.slot.inventory is PlayerInventory) {
val boundSlot = boundSlots.entries.find {
it.value == slot.index || it.key == slot.index
} ?: return
it.protectSilent()
val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true)
inventorySlot?.swapWithHotBar(handler, boundSlot.key)
}
}
@Subscribe
fun onLockUUID(it: HandledScreenKeyPressedEvent) {
if (!it.matches(TConfig.lockUUID)) return
@@ -156,16 +181,125 @@ object SlotLocking : FirmamentFeature {
it.cancel()
}
@Subscribe
fun onLockSlot(it: HandledScreenKeyPressedEvent) {
if (!it.matches(TConfig.lockSlot)) return
fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) {
val inventory = MC.handledScreen ?: return
inventory as AccessorHandledScreen
val slot = inventory.focusedSlot_Firmament
val storedSlot = storedLockingSlot ?: return
val slot = inventory.focusedSlot_Firmament ?: return
if (it.matches(TConfig.slotBind) && slot != storedSlot && slot != null && slot.isHotbar() != storedSlot.isHotbar()) {
storedLockingSlot = null
val hotBarSlot = if (slot.isHotbar()) slot else storedSlot
val invSlot = if (slot.isHotbar()) storedSlot else slot
val boundSlots = DConfig.data?.boundSlots ?: return
lockedSlots?.remove(hotBarSlot.index)
lockedSlots?.remove(invSlot.index)
boundSlots.entries.removeIf {
it.value == invSlot.index
}
boundSlots[hotBarSlot.index] = invSlot.index
DConfig.markDirty()
CommonSoundEffects.playSuccess()
return
}
if (it.matches(TConfig.lockSlot) && slot == storedSlot) {
storedLockingSlot = null
toggleSlotLock(slot)
return
}
if (it.matches(TConfig.slotBind)) {
storedLockingSlot = null
}
}
@Subscribe
fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) {
val boundSlots = DConfig.data?.boundSlots ?: return
fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true)
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
boundSlots.entries.forEach {
val hotbarSlot = findByIndex(it.key) ?: return@forEach
val inventorySlot = findByIndex(it.value) ?: return@forEach
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
event.context.drawBorder(hotbarSlot.x + sx,
hotbarSlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
event.context.drawBorder(inventorySlot.x + sx,
inventorySlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
}
}
@Subscribe
fun onRenderCurrentDraggingSlot(event: HandledScreenForegroundEvent) {
val draggingSlot = storedLockingSlot ?: return
val accScreen = event.screen as AccessorHandledScreen
val hoveredSlot = accScreen.focusedSlot_Firmament
?.takeIf { it.inventory is PlayerInventory }
?.takeIf { it == draggingSlot || it.isHotbar() != draggingSlot.isHotbar() }
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
val (borderX, borderY) = draggingSlot.lineCenter()
event.context.drawBorder(draggingSlot.x + sx, draggingSlot.y + sy, 16, 16, 0xFF00FF00u.toInt())
if (hoveredSlot == null) {
event.context.drawLine(
borderX + sx, borderY + sy,
event.mouseX, event.mouseY,
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
} else if (hoveredSlot != draggingSlot) {
val (hovX, hovY) = hoveredSlot.lineCenter()
event.context.drawLine(
borderX + sx, borderY + sy,
hovX + sx, hovY + sy,
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
event.context.drawBorder(hoveredSlot.x + sx,
hoveredSlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
}
}
fun Slot.lineCenter(): Pair<Int, Int> {
return if (isHotbar()) {
x + 9 to y
} else {
x + 9 to y + 17
}
}
fun Slot.isHotbar(): Boolean {
return index < 9
}
@Subscribe
fun onScreenChange(event: ScreenChangeEvent) {
storedLockingSlot = null
}
var storedLockingSlot: Slot? = null
fun toggleSlotLock(slot: Slot) {
val lockedSlots = lockedSlots ?: return
val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf()
if (slot.inventory is PlayerInventory) {
if (slot.index in lockedSlots) {
if (boundSlots.entries.removeIf {
it.value == slot.index || it.key == slot.index
}) {
// intentionally do nothing
} else if (slot.index in lockedSlots) {
lockedSlots.remove(slot.index)
} else {
lockedSlots.add(slot.index)
@@ -175,6 +309,23 @@ object SlotLocking : FirmamentFeature {
}
}
@Subscribe
fun onLockSlot(it: HandledScreenKeyPressedEvent) {
val inventory = MC.handledScreen ?: return
inventory as AccessorHandledScreen
val slot = inventory.focusedSlot_Firmament ?: return
if (slot.inventory !is PlayerInventory) return
if (it.matches(TConfig.slotBind)) {
storedLockingSlot = storedLockingSlot ?: slot
return
}
if (!it.matches(TConfig.lockSlot)) {
return
}
toggleSlotLock(slot)
}
@Subscribe
fun onRenderSlotOverlay(it: SlotRenderEvents.After) {
val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())

View File

@@ -29,6 +29,7 @@ import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.customgui.CustomGui
import moe.nea.firmament.util.customgui.customGui
import moe.nea.firmament.util.mc.CommonTextures
import moe.nea.firmament.util.mc.SlotUtils.clickRightMouseButton
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
@@ -103,26 +104,6 @@ object HotmPresets {
var hasScrolled = false
var hasAll = false
fun Slot.clickMiddleMouseButton(handler: ScreenHandler) {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
2,
SlotActionType.CLONE,
MC.player
)
}
fun Slot.clickRightMouseButton(handler: ScreenHandler) {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
1,
SlotActionType.PICKUP,
MC.player
)
}
override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (!hasScrolled) {
val slot = screen.screenHandler.getSlot(8)

View File

@@ -0,0 +1,26 @@
package moe.nea.firmament.util.mc
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.slot.Slot
object ScreenUtil {
private var lastScreen: Screen? = null
private var slotsByIndex: Map<SlotIndex, Slot> = mapOf()
data class SlotIndex(val index: Int, val isPlayerInventory: Boolean)
fun Screen.getSlotsByIndex(): Map<SlotIndex, Slot> {
if (this !is HandledScreen<*>) return mapOf()
if (lastScreen === this) return slotsByIndex
lastScreen = this
slotsByIndex = this.screenHandler.slots.associate {
SlotIndex(it.index, it.inventory is PlayerInventory) to it
}
return slotsByIndex
}
fun Screen.getSlotByIndex( index: Int, isPlayerInventory: Boolean): Slot? =
getSlotsByIndex()[SlotIndex(index, isPlayerInventory)]
}

View File

@@ -0,0 +1,35 @@
package moe.nea.firmament.util.mc
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import moe.nea.firmament.util.MC
object SlotUtils {
fun Slot.clickMiddleMouseButton(handler: ScreenHandler) {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
2,
SlotActionType.CLONE,
MC.player
)
}
fun Slot.swapWithHotBar(handler: ScreenHandler, hotbarIndex: Int) {
MC.interactionManager?.clickSlot(
handler.syncId, this.id,
hotbarIndex, SlotActionType.SWAP,
MC.player)
}
fun Slot.clickRightMouseButton(handler: ScreenHandler) {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
1,
SlotActionType.PICKUP,
MC.player
)
}
}

View File

@@ -1,8 +1,27 @@
package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
import me.shedaniel.math.Color
import org.joml.Matrix4f
import net.minecraft.client.gui.DrawContext
import moe.nea.firmament.util.MC
fun DrawContext.isUntranslatedGuiDrawContext(): Boolean {
return (matrices.peek().positionMatrix.properties() and Matrix4f.PROPERTY_IDENTITY.toInt()) != 0
}
fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Color) {
// TODO: push scissors
if (toY < fromY) {
drawLine(toX, toY, fromX, fromY, color)
return
}
RenderSystem.lineWidth(MC.window.scaleFactor.toFloat())
val buf = this.vertexConsumers.getBuffer(RenderInWorldContext.RenderLayers.LINES)
buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color)
.normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
buf.vertex(toX.toFloat(), toY.toFloat(), 0F).color(color.color)
.normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
this.draw()
}

View File

@@ -177,6 +177,8 @@
"firmament.config.slot-locking": "Slot Locking",
"firmament.config.slot-locking.lock": "Lock Slot",
"firmament.config.slot-locking.lock-uuid": "Lock UUID (Lock Item)",
"firmament.config.slot-locking.bind": "Bind Slot",
"firmament.config.slot-locking.require-quick-move": "Require Shift-Click for Bind",
"firmament.config.fixes.auto-sprint": "Auto Sprint",
"firmament.config.fixes.auto-sprint-keybinding": "Auto Sprint KeyBinding",
"firmament.config.fixes.auto-sprint-hud": "Sprint State Hud",