From 37e907f1df989847ff809d212de139ef389df444 Mon Sep 17 00:00:00 2001 From: Ukiyograin Date: Thu, 1 Oct 2026 10:35:14 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(input):=20=E9=BC=A0=E6=A0=87=E4=BE=A7?= =?UTF-8?q?=E9=94=AE/=E4=B8=AD=E9=94=AE=E5=8F=AF=E4=BD=9C=E4=B8=BA?= =?UTF-8?q?=E6=8C=89=E9=94=AE=E7=BB=91=E5=AE=9A=EF=BC=8C=E8=83=8C=E5=8C=85?= =?UTF-8?q?=E5=86=85=E4=B9=9F=E8=83=BD=E6=8D=A2=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原版 runTick 本就把鼠标按钮喂进 KeyBinding(setKeyBindState/onTick 传的都是 -100 + 按钮号),世界里绑在侧键上的热键栏能切;但有两处只认 键盘: - ClickGUI 绑定捕获只喂键盘 raw key,prism 的 raw key 通道又丢掉负值 (鼠标码恰在负数区),侧键既进不了捕获,也没有对应的 EventKey 派发。 - 背包里悬停换位 / 丢弃 / 选取方块走 GuiContainer.keyTyped,这条路只有 键盘事件会进,按侧键什么都不动,数字键却正常。 新增 BindKeys 统一码约定:与原版 GuiControls 写进 options.txt 的 -100 + 按钮一致(显示名 MOUSE4/MOUSE5),含按住式判断。Minecraft 上新增 鼠标事件 mixin:界面外派 EventKey,容器内经 GuiContainerAccessor 把这次 按键按进 keyTyped;ScaledGuiScreen 把中键/侧键喂给绑定界面捕获(死屏 不喂,沿用缓冲卫生规则)。显示名、.bind/.shortcut 解析、缩放与自由视角 的按住绑定一并走该约定。左/右键不参与绑定,留给界面点击与攻击使用。 --- .../features/command/CommandKeys.java | 12 ++- .../impl/interfaces/ClientSettings.java | 3 +- .../features/impl/optimizes/SmoothZoom.java | 3 +- .../features/impl/render/FreeLook.java | 5 +- .../features/settings/impl/BindKeys.java | 98 +++++++++++++++++++ .../mixin/MinecraftMixin_MouseBinds.java | 55 +++++++++++ .../mixin/accessor/GuiContainerAccessor.java | 17 ++++ .../top/fpsmaster/ui/click/MainPanel.java | 9 +- .../java/top/fpsmaster/ui/click/UiChrome.java | 12 +-- .../java/top/fpsmaster/ui/kit/EdgeUi.java | 12 +++ .../utils/render/gui/ScaledGuiScreen.java | 5 + src/main/resources/mixins.fpsmaster.json | 2 + 12 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java create mode 100644 src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java create mode 100644 src/main/java/top/fpsmaster/forge/mixin/accessor/GuiContainerAccessor.java diff --git a/src/main/java/top/fpsmaster/features/command/CommandKeys.java b/src/main/java/top/fpsmaster/features/command/CommandKeys.java index 1ddfbf1a..fc72b907 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..d81354e0 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,7 +35,7 @@ public void onRender3D(EventRender3D e) { if (Minecraft.getMinecraft().currentScreen != null) return; if (!perspectiveToggled) { - if (Keyboard.isKeyDown(bind.getValue())) { + if (BindKeys.isDown(bind.getValue())) { perspectiveToggled = true; if (Minecraft.getMinecraft().thePlayer != null) { cameraYaw = Minecraft.getMinecraft().thePlayer.rotationYaw; @@ -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..59725341 --- /dev/null +++ b/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java @@ -0,0 +1,98 @@ +package top.fpsmaster.features.settings.impl; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +/** + * 绑定值的按键码约定 + 显示名/解析,客户端所有绑定(模块键、BindSetting、AutoText、快捷键) + * 都走这一份。 + * + *

键盘沿用 LWJGL2 码(>0,0 = 未绑定);鼠标沿用原版 {@code GuiControls} 写进 options.txt 的 + * {@code -100 + 按钮序号}——原版 {@code Minecraft.runTick} 就是拿这个码去喂 {@code KeyBinding}, + * 两边共用一份码,用户在原版控制里绑的侧键才能和客户端互相认。 + * + *

左键(0)/右键(1) 不参与绑定:既是界面点击又是攻击/使用,绑了只会互相打架。可取的是 + * {@link #MIN_BINDABLE_BUTTON} 起的中键与侧键。 + */ +public final class BindKeys { + /** 原版鼠标码基准:{@code -100 + button}(button 是 LWJGL 的 0..n,0 = 左键)。 */ + public static final int MOUSE_BASE = -100; + + /** 可绑定的最小鼠标按钮:中键(LWJGL 的 2)。 */ + public static final int MIN_BINDABLE_BUTTON = 2; + + /** LWJGL 常见的鼠标按钮数,越界的码当无效。 */ + public static final int MAX_MOUSE_BUTTONS = 8; + + /** + * 鼠标码过 prism raw key 通道的中转基准。那条通道把负数当成「这一帧没按键」(缓冲用 -1 表示空), + * 鼠标码恰好在负数区,于是先借 1000+n 过桥,桥接层再 {@link #fromWire} 翻回来。 + */ + private static final int MOUSE_WIRE_BASE = 1000; + + private BindKeys() { + } + + public static boolean isMouse(int code) { + return code <= MOUSE_BASE; + } + + /** 鼠标码 → 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/MinecraftMixin_MouseBinds.java b/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java new file mode 100644 index 00000000..577c7b2e --- /dev/null +++ b/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java @@ -0,0 +1,55 @@ +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.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.forge.mixin.accessor.GuiContainerAccessor; +import top.fpsmaster.replay.ReplayDirectorIsolation; + +/** + * 鼠标侧键/中键的按键语义。 + * + *

世界里的原版按键原版已经照顾到了:runTick 会把鼠标码({@code -100 + 按钮})喂进 + * {@code KeyBinding.setKeyBindState / onTick},所以绑在侧键上的热键栏在世界里能切。死的是 + * 容器界面那一路——{@code GuiContainer.keyTyped} 只接键盘事件,背包里按侧键什么都不动, + * 数字键却照常换位。 + * + *

这里补两条:界面外派 {@link EventKey} 给客户端模块(和键盘路径同一份约定), + * 容器里则把这次按键按进容器的 {@code keyTyped},热键栏换位/丢弃/选取方块与键盘完全一致。 + */ +@Mixin(Minecraft.class) +public class MinecraftMixin_MouseBinds { + + @Shadow + public GuiScreen currentScreen; + + /** + * 挂在 runTick 里 {@code Mouse.getEventButton()} 之前——那正是原版逐个处理鼠标事件的位置, + * 当前事件(按钮号/按下与否)已经就绪,跟着原版的取法读同一份值就行。 + */ + @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lorg/lwjgl/input/Mouse;getEventButton()I")) + private void edge$mouseBinds(CallbackInfo ci) { + int button = Mouse.getEventButton(); + // 左/右键是攻击、使用与界面点击,不能当绑定——捕获端也只放行可绑定按钮。 + if (!Mouse.getEventButtonState() || !BindKeys.isBindableButton(button)) { + return; + } + int key = BindKeys.mouseCode(button); + if (currentScreen == null) { + if (!ReplayDirectorIsolation.isDirectorView()) { + EventDispatcher.dispatchEvent(new EventKey(key)); + } + } else if (currentScreen instanceof GuiContainer) { + ((GuiContainerAccessor) currentScreen).fpsmaster$keyTyped((char) 0, key); + } + } +} 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..b3d720a1 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; + // prism 的 raw key 通道只收非负值,鼠标码过桥时用的是 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..7c9ea7b6 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,15 @@ public static void keyTyped(char typedChar, int keyCode) { break; } } + + /** + * 鼠标中键/侧键当一次按键喂给绑定界面。prism 的 raw key 通道只收非负值,鼠标码(负数) + * 先借 {@link BindKeys#toWire} 过桥;左/右键不喂——「点一下开始捕获」本身就是左键, + * 喂进去会同一帧把自己绑掉。 + */ + 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..98d5cbc5 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,11 @@ 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..0f7d1584 100644 --- a/src/main/resources/mixins.fpsmaster.json +++ b/src/main/resources/mixins.fpsmaster.json @@ -97,6 +97,7 @@ "BlockEndPortalMixin_HideParticles", "MobSpawnerBaseLogicMixin_HideParticles", "MinecraftMixin_ReplayMenu", + "MinecraftMixin_MouseBinds", "RendererLivingEntityMixin_HideArmorStand", "RenderItemFrameMixin_Hide", "RenderArrowMixin_HideGround", @@ -109,6 +110,7 @@ "RenderGlobalMixin_SkyState", "RenderGlobalMixin_SkyProbe", "accessor.RenderAccessor", + "accessor.GuiContainerAccessor", "accessor.EntityArrowAccessor", "accessor.TextureMapAccessor", "accessor.ChunkProviderClientAccessor", From 77ee6eefb39aa70449998493c2d4d44da2397260 Mon Sep 17 00:00:00 2001 From: Ukiyograin Date: Thu, 1 Oct 2026 16:16:02 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(input):=20=E9=BC=A0=E6=A0=87=E4=BE=A7?= =?UTF-8?q?=E9=94=AE=E4=B8=A4=E5=A4=84=E6=AD=BB=E8=B7=AF=E2=80=94=E2=80=94?= =?UTF-8?q?=E7=A0=81=E5=88=A4=E5=AE=9A=E5=8F=8D=E5=90=91=E3=80=81=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=E6=B4=BE=E5=8F=91=E6=8C=82=E9=94=99=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BindKeys.isMouse 写成了 code <= -100,而鼠标码是 -100 + 按钮(-99..-93 全落在 键盘区):过桥拿到的负数被 prism 当「这一帧没按键」丢掉,ClickGUI 捕获永远收不到 侧键,绑定名也显示成 NONE。判定改为负数即鼠标码。 - 容器的按键派发挂在 runTick 的鼠标循环上,可界面打开时事件已被 handleInput 抽干、 逐个派给 handleMouseInput,那条循环收不到任何事件,背包里按侧键始终没反应。 改挂 GuiScreen.handleMouseInput(创客背包这类重写了它的屏也会 super 进来),把这次 按键按进 GuiContainer.keyTyped;取方块不补喂——原版 mouseClicked 自己认鼠标按钮, 补了会双踩。 --- .../features/command/CommandKeys.java | 4 +- .../features/settings/impl/BindKeys.java | 24 ++++------ .../mixin/MinecraftMixin_MouseBinds.java | 32 +++++--------- .../mixin/MixinGuiScreen_MouseBinds.java | 44 +++++++++++++++++++ .../top/fpsmaster/ui/click/MainPanel.java | 2 +- .../java/top/fpsmaster/ui/kit/EdgeUi.java | 3 +- .../utils/render/gui/ScaledGuiScreen.java | 3 +- src/main/resources/mixins.fpsmaster.json | 1 + 8 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 src/main/java/top/fpsmaster/forge/mixin/MixinGuiScreen_MouseBinds.java diff --git a/src/main/java/top/fpsmaster/features/command/CommandKeys.java b/src/main/java/top/fpsmaster/features/command/CommandKeys.java index fc72b907..e60d5fea 100644 --- a/src/main/java/top/fpsmaster/features/command/CommandKeys.java +++ b/src/main/java/top/fpsmaster/features/command/CommandKeys.java @@ -6,8 +6,8 @@ import java.util.Locale; /** - * Key-name parsing for {@code .bind} and {@code .shortcut set}: 键盘走 LWJGL2 的键盘表, - * 鼠标认 {@code MOUSE4} 这类名字(码值与显示名见 {@link BindKeys})。 + * 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 diff --git a/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java b/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java index 59725341..e7733663 100644 --- a/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java +++ b/src/main/java/top/fpsmaster/features/settings/impl/BindKeys.java @@ -4,37 +4,29 @@ import org.lwjgl.input.Mouse; /** - * 绑定值的按键码约定 + 显示名/解析,客户端所有绑定(模块键、BindSetting、AutoText、快捷键) - * 都走这一份。 - * - *

键盘沿用 LWJGL2 码(>0,0 = 未绑定);鼠标沿用原版 {@code GuiControls} 写进 options.txt 的 - * {@code -100 + 按钮序号}——原版 {@code Minecraft.runTick} 就是拿这个码去喂 {@code KeyBinding}, - * 两边共用一份码,用户在原版控制里绑的侧键才能和客户端互相认。 - * - *

左键(0)/右键(1) 不参与绑定:既是界面点击又是攻击/使用,绑了只会互相打架。可取的是 - * {@link #MIN_BINDABLE_BUTTON} 起的中键与侧键。 + * 绑定值的按键码约定:键盘走 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; - /** 可绑定的最小鼠标按钮:中键(LWJGL 的 2)。 */ + /** 可绑定的最小鼠标按钮:中键。 */ public static final int MIN_BINDABLE_BUTTON = 2; - /** LWJGL 常见的鼠标按钮数,越界的码当无效。 */ + /** 鼠标按钮数上限,越界当无效。 */ public static final int MAX_MOUSE_BUTTONS = 8; - /** - * 鼠标码过 prism raw key 通道的中转基准。那条通道把负数当成「这一帧没按键」(缓冲用 -1 表示空), - * 鼠标码恰好在负数区,于是先借 1000+n 过桥,桥接层再 {@link #fromWire} 翻回来。 - */ + /** 鼠标码过 prism raw key 通道的中转基准:那条通道拿 -1 表示「这一帧没按键」。 */ private static final int MOUSE_WIRE_BASE = 1000; private BindKeys() { } + /** 负数是鼠标码({@code -100 + 按钮}),正数才是键盘码。 */ public static boolean isMouse(int code) { - return code <= MOUSE_BASE; + return code < 0; } /** 鼠标码 → LWJGL 按钮号(0 = 左键);不是鼠标码时结果无意义。 */ diff --git a/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java b/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java index 577c7b2e..bd65140c 100644 --- a/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java +++ b/src/main/java/top/fpsmaster/forge/mixin/MinecraftMixin_MouseBinds.java @@ -2,7 +2,6 @@ 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.Shadow; @@ -12,19 +11,14 @@ import top.fpsmaster.event.EventDispatcher; import top.fpsmaster.event.events.EventKey; import top.fpsmaster.features.settings.impl.BindKeys; -import top.fpsmaster.forge.mixin.accessor.GuiContainerAccessor; import top.fpsmaster.replay.ReplayDirectorIsolation; /** - * 鼠标侧键/中键的按键语义。 + * 世界里的鼠标中键/侧键:补一个 {@link EventKey}(码 = {@code -100 + 按钮},见 {@link BindKeys}), + * 和键盘路径共用一条总线。 * - *

世界里的原版按键原版已经照顾到了:runTick 会把鼠标码({@code -100 + 按钮})喂进 - * {@code KeyBinding.setKeyBindState / onTick},所以绑在侧键上的热键栏在世界里能切。死的是 - * 容器界面那一路——{@code GuiContainer.keyTyped} 只接键盘事件,背包里按侧键什么都不动, - * 数字键却照常换位。 - * - *

这里补两条:界面外派 {@link EventKey} 给客户端模块(和键盘路径同一份约定), - * 容器里则把这次按键按进容器的 {@code keyTyped},热键栏换位/丢弃/选取方块与键盘完全一致。 + *

界面里的按键不走这里——runTick 的鼠标循环在界面打开时已被 handleInput 抽干,改由 + * {@link MixinGuiScreen_MouseBinds} 挂在 handleMouseInput 上。 */ @Mixin(Minecraft.class) public class MinecraftMixin_MouseBinds { @@ -33,23 +27,17 @@ public class MinecraftMixin_MouseBinds { public GuiScreen currentScreen; /** - * 挂在 runTick 里 {@code Mouse.getEventButton()} 之前——那正是原版逐个处理鼠标事件的位置, - * 当前事件(按钮号/按下与否)已经就绪,跟着原版的取法读同一份值就行。 + * 挂在 runTick 处理鼠标事件的位置:此处的当前事件(按钮号/按下与否)已经就绪。 */ @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lorg/lwjgl/input/Mouse;getEventButton()I")) private void edge$mouseBinds(CallbackInfo ci) { - int button = Mouse.getEventButton(); - // 左/右键是攻击、使用与界面点击,不能当绑定——捕获端也只放行可绑定按钮。 - if (!Mouse.getEventButtonState() || !BindKeys.isBindableButton(button)) { + if (currentScreen != null || !Mouse.getEventButtonState()) { return; } - int key = BindKeys.mouseCode(button); - if (currentScreen == null) { - if (!ReplayDirectorIsolation.isDirectorView()) { - EventDispatcher.dispatchEvent(new EventKey(key)); - } - } else if (currentScreen instanceof GuiContainer) { - ((GuiContainerAccessor) currentScreen).fpsmaster$keyTyped((char) 0, key); + 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/ui/click/MainPanel.java b/src/main/java/top/fpsmaster/ui/click/MainPanel.java index b3d720a1..bbd013e9 100644 --- a/src/main/java/top/fpsmaster/ui/click/MainPanel.java +++ b/src/main/java/top/fpsmaster/ui/click/MainPanel.java @@ -351,7 +351,7 @@ private String keyName(int keyCode) { } private static int normalizeKey(int keyCode) { - // prism 的 raw key 通道只收非负值,鼠标码过桥时用的是 1000+n,先翻回来再判空绑定。 + // 过桥的鼠标码(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/kit/EdgeUi.java b/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java index 7c9ea7b6..33666017 100644 --- a/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java +++ b/src/main/java/top/fpsmaster/ui/kit/EdgeUi.java @@ -100,8 +100,7 @@ public static void keyTyped(char typedChar, int keyCode) { } /** - * 鼠标中键/侧键当一次按键喂给绑定界面。prism 的 raw key 通道只收非负值,鼠标码(负数) - * 先借 {@link BindKeys#toWire} 过桥;左/右键不喂——「点一下开始捕获」本身就是左键, + * 鼠标中键/侧键当一次按键喂给绑定界面。左/右键不喂——「点一下开始捕获」本身就是左键, * 喂进去会同一帧把自己绑掉。 */ public static void mousePressed(int 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 98d5cbc5..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,8 +150,7 @@ public void handleMouseInput() throws IOException { if (eventButton != -1) { if (Mouse.getEventButtonState()) { inputState.pressButton(eventButton, logicalMouseX, logicalMouseY); - // 绑定按钮靠这口喂的 raw key 捕获中键/侧键;本屏在同一批事件里已经关掉的, - // 喂进去的键没有帧来清,会一直躺在共享缓冲里等下一个屏幕捡走,直接不喂。 + // 绑定捕获靠这口 raw key 喂中键/侧键;已在同一批事件里关掉的屏不喂,键会滞留到下一个屏幕。 if (mc.currentScreen == this) { EdgeUi.mousePressed(eventButton); } diff --git a/src/main/resources/mixins.fpsmaster.json b/src/main/resources/mixins.fpsmaster.json index 0f7d1584..bbd740ae 100644 --- a/src/main/resources/mixins.fpsmaster.json +++ b/src/main/resources/mixins.fpsmaster.json @@ -49,6 +49,7 @@ "MixinGuiNewChat", "MixinGuiPlayerOverlay", "MixinGuiScreen", + "MixinGuiScreen_MouseBinds", "MixinInventoryPlayer", "MixinInventoryEffectRenderer", "MixinItemRenderer", From ae7139418f4e7939d60bd47364fa660933c00fb3 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Thu, 1 Oct 2026 17:15:31 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(input):=20=E5=88=9B=E9=80=A0=E7=89=A9?= =?UTF-8?q?=E5=93=81=E6=A0=8F=E7=83=AD=E9=94=AE=E6=A0=8F=E6=8D=A2=E4=BD=8D?= =?UTF-8?q?=E5=90=8E=E7=9B=AE=E6=A0=87=E6=A0=BC=E6=9C=AA=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 非「物品栏」页悬停热键栏格按热键栏键(mode 2)时,原版 GuiContainerCreative 只给悬停格发 sendSlotPacket,换过去的目标热键栏格服务端仍是旧物品,放置时 露出原物品。侧键绑热键栏后容易撞上(数字键同样受影响)。在 handleMouseClick 末尾补发目标格。 顺带修正 FreeLook 按键判断处的缩进。 --- .../features/impl/render/FreeLook.java | 4 +-- ...ContainerCreativeMixin_HotbarSwapSync.java | 32 +++++++++++++++++++ src/main/resources/mixins.fpsmaster.json | 1 + 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/main/java/top/fpsmaster/forge/mixin/GuiContainerCreativeMixin_HotbarSwapSync.java 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 d81354e0..be797b5c 100644 --- a/src/main/java/top/fpsmaster/features/impl/render/FreeLook.java +++ b/src/main/java/top/fpsmaster/features/impl/render/FreeLook.java @@ -35,8 +35,8 @@ public void onRender3D(EventRender3D e) { if (Minecraft.getMinecraft().currentScreen != null) return; if (!perspectiveToggled) { - if (BindKeys.isDown(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; 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/resources/mixins.fpsmaster.json b/src/main/resources/mixins.fpsmaster.json index bbd740ae..90f9c051 100644 --- a/src/main/resources/mixins.fpsmaster.json +++ b/src/main/resources/mixins.fpsmaster.json @@ -44,6 +44,7 @@ "MixinFontRender", "MixinGuiChat", "MixinGuiContainer", + "GuiContainerCreativeMixin_HotbarSwapSync", "MixinGuiIngame", "MixinGuiMultiplayer", "MixinGuiNewChat",