diff --git a/src/main/java/top/fpsmaster/features/command/CommandKeys.java b/src/main/java/top/fpsmaster/features/command/CommandKeys.java index 1ddfbf1a..e60d5fea 100644 --- a/src/main/java/top/fpsmaster/features/command/CommandKeys.java +++ b/src/main/java/top/fpsmaster/features/command/CommandKeys.java @@ -1,11 +1,13 @@ package top.fpsmaster.features.command; import org.lwjgl.input.Keyboard; +import top.fpsmaster.features.settings.impl.BindKeys; import java.util.Locale; /** - * Key-name parsing for {@code .bind} and {@code .shortcut set}, over LWJGL2's keyboard tables. + * Key-name parsing for {@code .bind} and {@code .shortcut set}: 键盘走 LWJGL2 键盘表,鼠标认 + * {@code MOUSE4} 这类名字(码值与显示名见 {@link BindKeys})。 * *
{@link Keyboard#getKeyIndex(String)} returns {@link Keyboard#KEY_NONE} for anything it does not * know, which is the same value a deliberate unbind uses. {@link #parse(String)} therefore returns @@ -32,6 +34,10 @@ public static Integer parse(String raw) { return Keyboard.KEY_NONE; } } + Integer mouse = BindKeys.fromName(normalized); + if (mouse != null) { + return mouse; + } int key = Keyboard.getKeyIndex(normalized.toUpperCase(Locale.ROOT)); return key == Keyboard.KEY_NONE ? null : key; } @@ -41,7 +47,7 @@ public static String format(int key) { if (key == Keyboard.KEY_NONE) { return "NONE"; } - String name = Keyboard.getKeyName(key); - return name == null || name.isEmpty() ? "KEY_" + key : name; + String name = BindKeys.name(key); + return name == null ? "KEY_" + key : name; } } diff --git a/src/main/java/top/fpsmaster/features/impl/interfaces/ClientSettings.java b/src/main/java/top/fpsmaster/features/impl/interfaces/ClientSettings.java index 527fb8bb..e3ee6a89 100644 --- a/src/main/java/top/fpsmaster/features/impl/interfaces/ClientSettings.java +++ b/src/main/java/top/fpsmaster/features/impl/interfaces/ClientSettings.java @@ -10,6 +10,7 @@ import top.fpsmaster.exception.FileException; import top.fpsmaster.features.manager.Category; import top.fpsmaster.features.manager.Module; +import top.fpsmaster.features.settings.impl.BindKeys; import top.fpsmaster.features.settings.impl.BindSetting; import top.fpsmaster.features.settings.impl.BooleanSetting; import top.fpsmaster.features.settings.impl.ModeSetting; @@ -81,7 +82,7 @@ public static boolean isZoomBindDown() { return false; } - if (Keyboard.isKeyDown(zoomKey)) { + if (BindKeys.isDown(zoomKey)) { return true; } diff --git a/src/main/java/top/fpsmaster/features/impl/optimizes/SmoothZoom.java b/src/main/java/top/fpsmaster/features/impl/optimizes/SmoothZoom.java index d12c8b65..d59fc16d 100644 --- a/src/main/java/top/fpsmaster/features/impl/optimizes/SmoothZoom.java +++ b/src/main/java/top/fpsmaster/features/impl/optimizes/SmoothZoom.java @@ -7,6 +7,7 @@ import top.fpsmaster.event.events.EventUpdate; import top.fpsmaster.features.manager.Category; import top.fpsmaster.features.manager.Module; +import top.fpsmaster.features.settings.impl.BindKeys; import top.fpsmaster.features.settings.impl.BindSetting; import top.fpsmaster.features.settings.impl.BooleanSetting; import top.fpsmaster.features.settings.impl.NumberSetting; @@ -66,7 +67,7 @@ public void onUpdate(EventUpdate e) { } public boolean isZoomKeyDown() { - return Keyboard.isKeyDown(zoomBind.getValue()); + return BindKeys.isDown(zoomBind.getValue()); } public static boolean isZoomKeyActive() { diff --git a/src/main/java/top/fpsmaster/features/impl/render/FreeLook.java b/src/main/java/top/fpsmaster/features/impl/render/FreeLook.java index 29384115..be797b5c 100644 --- a/src/main/java/top/fpsmaster/features/impl/render/FreeLook.java +++ b/src/main/java/top/fpsmaster/features/impl/render/FreeLook.java @@ -7,6 +7,7 @@ import top.fpsmaster.event.events.EventRender3D; import top.fpsmaster.features.manager.Category; import top.fpsmaster.features.manager.Module; +import top.fpsmaster.features.settings.impl.BindKeys; import top.fpsmaster.features.settings.impl.BindSetting; public class FreeLook extends Module { @@ -34,8 +35,8 @@ public void onRender3D(EventRender3D e) { if (Minecraft.getMinecraft().currentScreen != null) return; if (!perspectiveToggled) { - if (Keyboard.isKeyDown(bind.getValue())) { - perspectiveToggled = true; + if (BindKeys.isDown(bind.getValue())) { + perspectiveToggled = true; if (Minecraft.getMinecraft().thePlayer != null) { cameraYaw = Minecraft.getMinecraft().thePlayer.rotationYaw; cameraPitch = Minecraft.getMinecraft().thePlayer.rotationPitch; @@ -43,7 +44,7 @@ public void onRender3D(EventRender3D e) { previousPerspective = Minecraft.getMinecraft().gameSettings.hideGUI; Minecraft.getMinecraft().gameSettings.thirdPersonView = 1; } - } else if (!Keyboard.isKeyDown(bind.getValue())) { + } else if (!BindKeys.isDown(bind.getValue())) { perspectiveToggled = false; Minecraft.getMinecraft().gameSettings.thirdPersonView = previousPerspective ? 1 : 0; } diff --git a/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java b/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java new file mode 100644 index 00000000..e7733663 --- /dev/null +++ b/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java @@ -0,0 +1,90 @@ +package top.fpsmaster.features.settings.impl; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +/** + * 绑定值的按键码约定:键盘走 LWJGL2 码(>0,0 = 未绑定);鼠标走原版 {@code GuiControls} + * 写进 options.txt 的 {@code -100 + 按钮序号},与原版控制里绑的侧键共用一份码。左/右键留给 + * 界面点击与攻击,只有中键和侧键可绑。 + */ +public final class BindKeys { + /** 原版鼠标码基准:{@code -100 + button}(button 是 LWJGL 的 0..n,0 = 左键)。 */ + public static final int MOUSE_BASE = -100; + + /** 可绑定的最小鼠标按钮:中键。 */ + public static final int MIN_BINDABLE_BUTTON = 2; + + /** 鼠标按钮数上限,越界当无效。 */ + public static final int MAX_MOUSE_BUTTONS = 8; + + /** 鼠标码过 prism raw key 通道的中转基准:那条通道拿 -1 表示「这一帧没按键」。 */ + private static final int MOUSE_WIRE_BASE = 1000; + + private BindKeys() { + } + + /** 负数是鼠标码({@code -100 + 按钮}),正数才是键盘码。 */ + public static boolean isMouse(int code) { + return code < 0; + } + + /** 鼠标码 → LWJGL 按钮号(0 = 左键);不是鼠标码时结果无意义。 */ + public static int mouseButton(int code) { + return code - MOUSE_BASE; + } + + public static int mouseCode(int button) { + return MOUSE_BASE + button; + } + + /** 这个鼠标按钮能不能当绑定:中键与侧键可以,左右键留给界面与战斗。 */ + public static boolean isBindableButton(int button) { + return button >= MIN_BINDABLE_BUTTON && button < MAX_MOUSE_BUTTONS; + } + + /** 鼠标码 → prism raw key 通道能接受的非负值(键盘码原样过)。 */ + public static int toWire(int code) { + return isMouse(code) ? MOUSE_WIRE_BASE + mouseButton(code) : code; + } + + /** {@link #toWire} 的逆运算。 */ + public static int fromWire(int code) { + return code >= MOUSE_WIRE_BASE ? mouseCode(code - MOUSE_WIRE_BASE) : code; + } + + /** 按住式绑定(缩放、自由视角)的按下判断。 */ + public static boolean isDown(int code) { + if (isMouse(code)) { + int button = mouseButton(code); + return isBindableButton(button) && Mouse.isButtonDown(button); + } + return code > 0 && Keyboard.isKeyDown(code); + } + + /** 显示名;未绑定或认不出的码返回 {@code null},由调用点决定兜底文案。 */ + public static String name(int code) { + if (isMouse(code)) { + int button = mouseButton(code); + return isBindableButton(button) ? "MOUSE" + (button + 1) : null; + } + if (code <= 0) { + return null; + } + String name = Keyboard.getKeyName(code); + return name == null || name.isEmpty() || "NONE".equalsIgnoreCase(name) ? null : name; + } + + /** 名字 → 码,认 "MOUSE4" 这类鼠标名(1 起数,和显示名一致);认不出返回 {@code null}。 */ + public static Integer fromName(String name) { + if (name == null || !name.regionMatches(true, 0, "MOUSE", 0, 5)) { + return null; + } + try { + int button = Integer.parseInt(name.substring(5).trim()) - 1; + return isBindableButton(button) ? Integer.valueOf(mouseCode(button)) : null; + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/src/main/java/top/fpsmaster/forge/mixin/GuiContainerCreativeMixin_HotbarSwapSync.java b/src/main/java/top/fpsmaster/forge/mixin/GuiContainerCreativeMixin_HotbarSwapSync.java new file mode 100644 index 00000000..6d975fc6 --- /dev/null +++ b/src/main/java/top/fpsmaster/forge/mixin/GuiContainerCreativeMixin_HotbarSwapSync.java @@ -0,0 +1,32 @@ +package top.fpsmaster.forge.mixin; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.inventory.GuiContainerCreative; +import net.minecraft.inventory.Slot; +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; + +/** + * 创造模式物品栏的热键栏换位只同步了一半:非「物品栏」页里悬停热键栏格按热键栏键(mode 2), + * 原版走 {@code handleMouseClick} 末尾分支,本地两格都换了,却只给悬停格发 + * {@code sendSlotPacket}——目标热键栏格从没发给服务端,服务端那格还是旧物品,一放置就露馅。 + * 原版用数字键也一样,鼠标侧键绑热键栏后更容易撞上。 + * + *
挂 TAIL:物品格(创造物品表)那支和丢弃等分支都中途 return 不会走到这里;「物品栏」页那支 + * 已经 detectAndSendChanges 过,这里再发一次同值包无害。 + */ +@Mixin(GuiContainerCreative.class) +public class GuiContainerCreativeMixin_HotbarSwapSync { + + @Inject(method = "handleMouseClick", at = @At("TAIL")) + private void edge$syncSwappedHotbarSlot(Slot slotIn, int slotId, int clickedButton, int clickType, CallbackInfo ci) { + if (clickType != 2 || slotIn == null || clickedButton < 0 || clickedButton >= 9) { + return; + } + Minecraft mc = Minecraft.getMinecraft(); + // inventoryContainer 里热键栏 i 是 36 + i。 + mc.playerController.sendSlotPacket(mc.thePlayer.inventory.getStackInSlot(clickedButton), 36 + clickedButton); + } +} diff --git a/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java b/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java new file mode 100644 index 00000000..bd65140c --- /dev/null +++ b/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java @@ -0,0 +1,43 @@ +package top.fpsmaster.forge.mixin; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import org.lwjgl.input.Mouse; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import top.fpsmaster.event.EventDispatcher; +import top.fpsmaster.event.events.EventKey; +import top.fpsmaster.features.settings.impl.BindKeys; +import top.fpsmaster.replay.ReplayDirectorIsolation; + +/** + * 世界里的鼠标中键/侧键:补一个 {@link EventKey}(码 = {@code -100 + 按钮},见 {@link BindKeys}), + * 和键盘路径共用一条总线。 + * + *
界面里的按键不走这里——runTick 的鼠标循环在界面打开时已被 handleInput 抽干,改由 + * {@link MixinGuiScreen_MouseBinds} 挂在 handleMouseInput 上。 + */ +@Mixin(Minecraft.class) +public class MinecraftMixin_MouseBinds { + + @Shadow + public GuiScreen currentScreen; + + /** + * 挂在 runTick 处理鼠标事件的位置:此处的当前事件(按钮号/按下与否)已经就绪。 + */ + @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lorg/lwjgl/input/Mouse;getEventButton()I")) + private void edge$mouseBinds(CallbackInfo ci) { + if (currentScreen != null || !Mouse.getEventButtonState()) { + return; + } + int button = Mouse.getEventButton(); + if (!BindKeys.isBindableButton(button) || ReplayDirectorIsolation.isDirectorView()) { + return; + } + EventDispatcher.dispatchEvent(new EventKey(BindKeys.mouseCode(button))); + } +} diff --git a/src/main/java/top/fpsmaster/forge/mixin/MixinGuiScreen_MouseBinds.java b/src/main/java/top/fpsmaster/forge/mixin/MixinGuiScreen_MouseBinds.java new file mode 100644 index 00000000..1b4bdb01 --- /dev/null +++ b/src/main/java/top/fpsmaster/forge/mixin/MixinGuiScreen_MouseBinds.java @@ -0,0 +1,44 @@ +package top.fpsmaster.forge.mixin; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.inventory.GuiContainer; +import org.lwjgl.input.Mouse; +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; +import top.fpsmaster.features.settings.impl.BindKeys; +import top.fpsmaster.forge.mixin.accessor.GuiContainerAccessor; + +/** + * 容器界面里的鼠标中键/侧键:原版只有键盘事件会进 {@code GuiContainer.keyTyped},背包里按 + * 侧键(在原版控制里绑给热键栏/丢弃)什么都不动。这里把这次按键按进 keyTyped,与键盘一致。 + * + *
挂 handleMouseInput 而不是 runTick 的鼠标循环:界面打开时鼠标事件由 + * {@code GuiScreen.handleInput} 抽干后逐个派到这里,runTick 那条循环收不到。 + */ +@Mixin(GuiScreen.class) +public class MixinGuiScreen_MouseBinds { + + @Inject(method = "handleMouseInput", at = @At("HEAD")) + private void edge$mouseKeyTyped(CallbackInfo ci) { + GuiScreen self = (GuiScreen) (Object) this; + // 按键抬起也带着同一个按钮号,不滤掉的话换位会连做两下、互相抵消。 + if (!Mouse.getEventButtonState() || !(self instanceof GuiContainer) + || Minecraft.getMinecraft().currentScreen != self) { + return; + } + int button = Mouse.getEventButton(); + if (!BindKeys.isBindableButton(button)) { + return; + } + // 取方块是原版唯一认鼠标按钮的容器键(mouseClicked 里按 getKeyCode() + 100 判), + // 再喂一次 keyTyped 会双踩同一次取方块。 + Minecraft mc = Minecraft.getMinecraft(); + if (BindKeys.mouseCode(button) == mc.gameSettings.keyBindPickBlock.getKeyCode()) { + return; + } + ((GuiContainerAccessor) self).fpsmaster$keyTyped((char) 0, BindKeys.mouseCode(button)); + } +} diff --git a/src/main/java/top/fpsmaster/forge/mixin/accessor/GuiContainerAccessor.java b/src/main/java/top/fpsmaster/forge/mixin/accessor/GuiContainerAccessor.java new file mode 100644 index 00000000..4e0a1987 --- /dev/null +++ b/src/main/java/top/fpsmaster/forge/mixin/accessor/GuiContainerAccessor.java @@ -0,0 +1,17 @@ +package top.fpsmaster.forge.mixin.accessor; + +import net.minecraft.client.gui.inventory.GuiContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** + * Reaches {@code GuiContainer.keyTyped}, which is protected. + * + *
原版只把键盘事件送进 keyTyped;鼠标绑定的按键要在背包里换位/丢弃/选取方块,就得自己按一次。 + */ +@Mixin(GuiContainer.class) +public interface GuiContainerAccessor { + + @Invoker("keyTyped") + void fpsmaster$keyTyped(char typedChar, int keyCode); +} diff --git a/src/main/java/top/fpsmaster/ui/click/MainPanel.java b/src/main/java/top/fpsmaster/ui/click/MainPanel.java index 64de53e0..bbd013e9 100644 --- a/src/main/java/top/fpsmaster/ui/click/MainPanel.java +++ b/src/main/java/top/fpsmaster/ui/click/MainPanel.java @@ -10,6 +10,7 @@ import top.fpsmaster.features.settings.Setting; import top.fpsmaster.features.settings.impl.AutoTextEntry; import top.fpsmaster.features.settings.impl.AutoTextSetting; +import top.fpsmaster.features.settings.impl.BindKeys; import top.fpsmaster.features.settings.impl.BindSetting; import top.fpsmaster.features.settings.impl.BooleanSetting; import top.fpsmaster.features.settings.impl.ColorSetting; @@ -345,11 +346,13 @@ private static boolean matchesSearch(Module module, String rawQuery) { } private String keyName(int keyCode) { - String name = keyCode == 0 ? null : Keyboard.getKeyName(keyCode); - return name == null || name.isEmpty() ? FPSMaster.i18n.get("clickgui.bind.none") : name; + String name = BindKeys.name(keyCode); + return name == null ? FPSMaster.i18n.get("clickgui.bind.none") : name; } private static int normalizeKey(int keyCode) { - return keyCode == Keyboard.KEY_BACK || keyCode == Keyboard.KEY_DELETE ? 0 : keyCode; + // 过桥的鼠标码(1000+n)先翻回来,再判空绑定。 + int code = BindKeys.fromWire(keyCode); + return code == Keyboard.KEY_BACK || code == Keyboard.KEY_DELETE ? 0 : code; } } diff --git a/src/main/java/top/fpsmaster/ui/click/UiChrome.java b/src/main/java/top/fpsmaster/ui/click/UiChrome.java index 4fbf7a98..0914de8a 100644 --- a/src/main/java/top/fpsmaster/ui/click/UiChrome.java +++ b/src/main/java/top/fpsmaster/ui/click/UiChrome.java @@ -1,7 +1,7 @@ package top.fpsmaster.ui.click; -import org.lwjgl.input.Keyboard; import top.fpsmaster.FPSMaster; +import top.fpsmaster.features.settings.impl.BindKeys; import top.fpsmaster.font.impl.UFontRenderer; import top.fpsmaster.ui.kit.EdgeUi; import top.fpsmaster.utils.render.draw.Hover; @@ -187,14 +187,8 @@ public static void searchBox(float x, float y, float width, float height, boolea } public static String keyName(int key) { - if (key <= 0) { - return FPSMaster.i18n.get("clickgui.bind.none"); - } - String name = Keyboard.getKeyName(key); - if (name == null || name.isEmpty() || "NONE".equalsIgnoreCase(name)) { - return FPSMaster.i18n.get("clickgui.bind.none"); - } - return name; + String name = BindKeys.name(key); + return name == null ? FPSMaster.i18n.get("clickgui.bind.none") : name; } public static float keyChipWidth(String text) { diff --git a/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java b/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java index 8cdb0cde..33666017 100644 --- a/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java +++ b/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java @@ -1,6 +1,7 @@ package top.fpsmaster.ui.kit; import top.fpsmaster.features.impl.interfaces.ClientSettings; +import top.fpsmaster.features.settings.impl.BindKeys; import top.fpsmaster.utils.render.gui.ScaledGuiScreen; import top.fpsmaster.prism.input.FrameInput; import top.fpsmaster.prism.input.Keys; @@ -97,4 +98,14 @@ public static void keyTyped(char typedChar, int keyCode) { break; } } + + /** + * 鼠标中键/侧键当一次按键喂给绑定界面。左/右键不喂——「点一下开始捕获」本身就是左键, + * 喂进去会同一帧把自己绑掉。 + */ + public static void mousePressed(int button) { + if (BindKeys.isBindableButton(button)) { + fallback.pressRawKey(BindKeys.toWire(BindKeys.mouseCode(button))); + } + } } diff --git a/src/main/java/top/fpsmaster/utils/render/gui/ScaledGuiScreen.java b/src/main/java/top/fpsmaster/utils/render/gui/ScaledGuiScreen.java index df077ea7..f3d0f683 100644 --- a/src/main/java/top/fpsmaster/utils/render/gui/ScaledGuiScreen.java +++ b/src/main/java/top/fpsmaster/utils/render/gui/ScaledGuiScreen.java @@ -150,6 +150,10 @@ public void handleMouseInput() throws IOException { if (eventButton != -1) { if (Mouse.getEventButtonState()) { inputState.pressButton(eventButton, logicalMouseX, logicalMouseY); + // 绑定捕获靠这口 raw key 喂中键/侧键;已在同一批事件里关掉的屏不喂,键会滞留到下一个屏幕。 + if (mc.currentScreen == this) { + EdgeUi.mousePressed(eventButton); + } mousePressed(logicalMouseX, logicalMouseY, eventButton); } else { inputState.releaseButton(eventButton); diff --git a/src/main/resources/mixins.fpsmaster.json b/src/main/resources/mixins.fpsmaster.json index 1c869054..90f9c051 100644 --- a/src/main/resources/mixins.fpsmaster.json +++ b/src/main/resources/mixins.fpsmaster.json @@ -44,11 +44,13 @@ "MixinFontRender", "MixinGuiChat", "MixinGuiContainer", + "GuiContainerCreativeMixin_HotbarSwapSync", "MixinGuiIngame", "MixinGuiMultiplayer", "MixinGuiNewChat", "MixinGuiPlayerOverlay", "MixinGuiScreen", + "MixinGuiScreen_MouseBinds", "MixinInventoryPlayer", "MixinInventoryEffectRenderer", "MixinItemRenderer", @@ -97,6 +99,7 @@ "BlockEndPortalMixin_HideParticles", "MobSpawnerBaseLogicMixin_HideParticles", "MinecraftMixin_ReplayMenu", + "MinecraftMixin_MouseBinds", "RendererLivingEntityMixin_HideArmorStand", "RenderItemFrameMixin_Hide", "RenderArrowMixin_HideGround", @@ -109,6 +112,7 @@ "RenderGlobalMixin_SkyState", "RenderGlobalMixin_SkyProbe", "accessor.RenderAccessor", + "accessor.GuiContainerAccessor", "accessor.EntityArrowAccessor", "accessor.TextureMapAccessor", "accessor.ChunkProviderClientAccessor",