控制设置界面

基本信息

属性 值
主界面类 com.blamejared.controlling.client.gui.GuiNewControls
列表类 com.blamejared.controlling.client.gui.GuiNewKeyBindingList
继承 GuiNewControls extends GuiControls(原版),GuiNewKeyBindingList extends GuiKeyBindingList(原版)
侧别 @SideOnly(Side.CLIENT)
注入方式 ClientEventHandler 监听 GuiOpenEvent 替换 event.gui
顶部选项 INVERT_MOUSE、SENSITIVITY、TOUCHSCREEN

功能

本 mod 不注册任何按键,它改造的是按键设置界面本身:接管原版 GuiControls, 加入搜索、过滤、排序、单条重置、组合键编辑与可视化键盘。

接管方式

ClientEventHandler.openGui(GuiOpenEvent) 的判定是 event.gui instanceof GuiControls && !(event.gui instanceof GuiNewControls), 满足则替换为 new GuiNewControls(Minecraft.getMinecraft().currentScreen, Minecraft.getMinecraft().gameSettings)。 整个逻辑包在 try/catch 中,异常仅 printStackTrace(),不会阻断界面打开。

事件在 Controlling.init 中通过 MinecraftForge.EVENT_BUS.register(new ClientEventHandler()) 注册, 且 init 在服务端直接 return,所以这一切都是纯客户端的。

按钮 ID 总表

底部与顶部按钮(GuiNewControls)

常量 ID 位置 初始文案
DONE_BUTTON_ID 1001 右下 gui.done
RESET_ALL_KEYS_BUTTON_ID 1002 左下 controls.resetAll
SHOW_UNBOUD_BUTTON_ID 1003 右下上一行 options.showNone
SHOW_CONFLICTS_BUTTON_ID 1004 右下上一行左 options.showConflicts
SEARCH_KEYNAME_BUTTON_ID 1005 左下复选框 options.key
SEARCH_CATEGORYNAME_BUTTON_ID 1006 左下复选框 options.category
VISUAL_KEYBOARD_BUTTON_ID 1007 右下上两行 options.keyMap
SORT_TYPE_BUTTON_ID 1008 右下上两行右 options.sort + ": " + 下一个排序名

另有 3 个原版 GameSettings.Options(INVERT_MOUSE / SENSITIVITY / TOUCHSCREEN)按 returnEnumOrdinal() 作为按钮 ID,ID 小于 100 时按原版逻辑切换选项值。

每行按键的按钮(GuiNewKeyBindingList.KeyEntry)

按钮 ID 宽 说明
btnChangeKeyBinding 2000 95 进入按键编辑态,文案为组合显示名或原版键名
btnResetKeyBinding 2001 50 controls.reset,重置该行
btnVisualKeyboard 2002 18 纯图标按钮(代码绘制的键盘小图标),打开 可视化键盘

横坐标分别为 x + 105(改键)、x + 204(键盘图标)、x + 224(重置)。

恢复全部按键

按钮 ID 1002 采用两步确认:

  1. 第一次点击:confirmingReset = true,文案变为 options.confirmReset (英文 Click to confirm reset!),直接 return 不执行任何修改;
  2. 第二次点击:confirmingReset = false,文案还原为 controls.resetAll,然后 遍历 mc.gameSettings.keyBindings:
    • ComboKeyBinding → controlling$setToDefault()(同时恢复键码和修饰键);
    • 其他 → keyBinding.setKeyCode(keyBinding.getKeyCodeDefault()); 之后调用 this.options.saveOptions() 与 KeyBinding.resetKeyBindingArrayAndHash()。

按钮可用性:drawScreen 每帧遍历 options.keyBindings 计算 isModified—— 组合键走 controlling$isSetToDefaultValue(),普通键比较 getKeyCode() 与 getKeyCodeDefault()。 isModified == false 时按钮禁用、文案保持 controls.resetAll,并把 confirmingReset 强制复位。

单行重置(ID 2001)的可用性判定逻辑与之完全一致,但执行时额外调用 mc.gameSettings.setOptionKeyBinding(this.keybinding, this.keybinding.getKeyCodeDefault()) 再 KeyBinding.resetKeyBindingArrayAndHash()。

组合键录入流程

这是本界面最复杂的部分,源码用"待定(pending)+ 释放后才提交"两段式实现。

为什么延迟提交:按键编辑期间玩家按下的 Ctrl、G 等会立刻触发游戏内行为。 先记下待定绑定、等玩家松开后再写入,可以避免误触发。

阶段 行为
1. 进入编辑 selectKeyBinding(keyBinding, showVisualKeyboard):设 buttonId,若走键盘面板则把当前修饰键带入面板
2. 玩家按修饰键 keyTyped 中 KeyModifier.isKeyCodeModifier(keyCode) 命中且为组合键 → 记 selectedModifier / selectedModifierKeyCode,不关闭编辑态
3. 玩家按主键 schedulePendingBinding(buttonId, inputKeyCode, getSelectedModifierForBinding()) 记下三元组,仍不关闭
4. 释放 applyPendingBindingIfReleased()(由 updateScreen 每帧调用)检测到输入已松开 → 写入

getSelectedModifierForBinding() 的推断规则:若显式选过修饰键就用它, 否则退回 KeyModifier.getActiveModifier()(即"此刻正按着哪个修饰键")。 走可视化键盘时则取键盘面板的当前修饰键。

isPendingInputStillActive() 判定"还没松开"的三种情况:

  • pendingModifier != NONE 且 pendingModifier.isActive();
  • pendingKeyCode <= -100(鼠标键编码,-100 + 按钮号)且该鼠标键仍按下;
  • pendingKeyCode 落在 Keyboard.KEY_NONE 与 256 之间的键盘键码区间且仍按下。

注意 typedChar + 256 编码的字符键不在上述第三种判定内,会被视为已松开。 keyTyped 中 inputKeyCode 的取值是 keyCode != KEY_NONE ? keyCode : (typedChar > 0 ? typedChar + 256 : KEY_NONE)。

裸修饰键单独绑定:captureModifierOnlyBinding() 处理"玩家只按了 Ctrl 就松手"的情况, 调 schedulePendingBinding(buttonId, selectedModifierKeyCode, NONE), 即把该修饰键本身绑成裸热键。

按 ESC 取消:对组合键调用 controlling$setKeyModifierAndCode(KeyModifier.NONE, Keyboard.KEY_NONE) (清空绑定),再 options.setOptionKeyBinding(buttonId, Keyboard.KEY_NONE),并清空全部待定状态。

鼠标键绑定:在 mouseClicked 中,buttonId != null 时若为组合键则 schedulePendingBinding(buttonId, -100 + mb, getSelectedModifierForBinding()), 即鼠标键与修饰键同样可以组成组合键。

统一收尾:键盘、鼠标两条录入路径结束后都会调用 KeyBinding.resetKeyBindingArrayAndHash() 并刷新 field_152177_g(Minecraft.getSystemTime(),原版用于与其他 mod 协调按键冲突的时间戳)。

搜索框

  • 宽度 148、高 18,setCanLoseFocus(true);
  • 文本变化时(updateScreen 中与 lastSearch 比对)触发 filterKeys();
  • 焦点状态下右键(mb == 1)点击会清空文本;
  • ESC 在焦点状态下只取消焦点;不在焦点状态且未开键盘面板时,ESC 走 mc.displayGuiScreen(null) + mc.setIngameFocus()。

1.7.10 混淆成员的绕行

1.7.10 中 GuiScreen 的若干方法仍未被混淆回可读名,源码显式绕行:

调用 实际含义 处理方式
guiNewKeyBindingList.func_148179_a(mx, my, mb) GuiListExtended.mouseClicked 直接调用
guiNewKeyBindingList.func_148181_b(...) GuiListExtended.mouseReleased 直接调用
superSuperMouseClicked 原版 GuiScreen.mouseClicked 重写实现,手工 post GuiScreenEvent.ActionPerformedEvent.Pre/Post 并调用 func_146113_a(点击音效)
superSuperMouseReleased 原版 GuiScreen.mouseReleased 重写实现 selectedButton.mouseReleased
superSuperKeyTyped 原版 GuiScreen.keyTyped 重写实现 ESC 关闭界面

superSuperMouseClicked 之所以必须重写:它需要显式向 Forge 事件总线投递 GuiScreenEvent.ActionPerformedEvent.Pre 与 .Post,否则其他 mod 的界面事件钩子会失效。 转发过程用 try/catch 包住 IOException,失败时以 Controlling.LOGGER.error 记录。

buttonList 与 selectedButton 两个原版私有字段通过 META-INF/controlling_at.cfg 访问转换器开放。

列表渲染

GuiNewKeyBindingList 的尺寸与行为:

属性 值
width / right controls.width + 45
top 63
bottom controls.height - 80
getScrollBarX() 原版 + 35
getListWidth() 原版 + 32

构造时克隆 gameSettings.keyBindings 并 Arrays.sort,跳过分类名以 .hidden 结尾的按键, 按分类变化插入 CategoryEntry 标题行,每条按键生成一个 KeyEntry。

按键名标签右对齐到 x + 95,最大宽度 91;超宽时用 trimWithEllipsis 截断并追加 ...。 鼠标悬停在被截断的标签上时记录到 hoveredKeyDescription, 由控制界面绘制 tooltip(开启可视化键盘时则改为绘制键盘 tooltip)。

冲突标记:行首/行尾的括号颜色按严重度区分——

标记 颜色 条件
> … <(黄色 + 下划线) EnumChatFormatting.YELLOW 该行正处于编辑态(controlsScreen.buttonId == keybinding)
[ … ] 金色 EnumChatFormatting.GOLD 硬冲突且 modConflict 仍为真,即同键码不同修饰键的轻度冲突
[ … ] 红色 EnumChatFormatting.RED 存在完全相同的键码 + 修饰键绑定

计算时 modConflict 初值为 true,对每个冲突项取 &= controlling$hasKeyCodeModifierConflict(key);一旦出现非 ComboKeyBinding 的同键码绑定, 会直接 break 且不参与该标志的收窄,因此显示为红色。

scrollToKeyBinding(keyBinding) 可把指定绑定滚动到视口中央; showKeyBinding 在目标当前被过滤掉时,会先清空搜索框、重置 displayMode 为 ALL、 复位两个显示按钮的文案,再重新过滤并滚动。

相关条目