主菜单替换系统
DummyCore 用一整套客户端 API 接管原版主菜单,允许其他 mod 注册自定义主菜单,玩家可通过按钮切换。
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 特殊机制(客户端 GUI) |
| 触发条件 | 每次主菜单 InitGuiEvent.Pre / Post |
| 标记接口 | DummyCore/Utils/IMainMenu.java(空接口,仅作类型标记) |
| 注册表 | DummyCore/Client/MainMenuRegistry.java |
| 事件处理 | DummyCore/Utils/DummyEventHandler.java:50-82 |
| 相关配置 | mainMenuID、hideSwitchMenuButton |
行为
1. 用自定义菜单替换原版主菜单
DummyEventHandler.onMainMenuGUISetup(InitGuiEvent.Pre)(DummyEventHandler.java:50-63):
- 若当前 GUI 恰好是原版
GuiMainMenu,直接event.setCanceled(true)取消其初始化, 然后调用MainMenuRegistry.newMainMenu(DummyConfig.getMainMenu())打开配置里指定的下标菜单。 - 若当前 GUI 实现了
IMainMenu,但与配置指定的下标对应的类不一致,同样取消并切换过去。
注意 DummyCore 自己没有新写一套主菜单,而是让两个类继承原版 GuiMainMenu 再实现 IMainMenu
(GuiMainMenuVanilla、GuiMainMenuOld,两者都 extends GuiMainMenu implements IMainMenu)。
因此菜单外观仍是原版,只是走 DummyCore 的注册表。
2. 内置的两个菜单
由 NetProxy_Client.registerInfo()(NetProxy_Client.java:45-48)在 preInit 阶段注册:
| 下标 | 类 | 名称 | 描述 |
|---|---|---|---|
| 0 | GuiMainMenuVanilla |
[DC] Vanilla |
Just a simple vanilla MC gui. |
| 1 | GuiMainMenuOld |
[DC] Old Vanilla |
An old MC gui. |
由于 DummyCore 自己就注册了这两个,其它 mod 的菜单会被追加在下标 2 及之后。
这一点保证了即使没有任何其它 mod 注册主菜单,menuList 也不会为空,主菜单仍能正常打开。
3. 切换按钮
DummyEventHandler.onMainMenuGUISetup(InitGuiEvent.Post)(DummyEventHandler.java:68-82):
当当前界面实现 IMainMenu 且 hideSwitchMenuButton == false 时,向 buttonList 追加一个
GuiButton_ChangeGUI,其 ID 为 65535,位置硬编码为:
| 属性 | 值 |
|---|---|
| 文本 | Change Main Menu |
| 按钮 ID | 65535 |
| X | event.gui.width / 2 + 104 |
| Y | event.gui.height / 4 + 24 + 72 |
| 宽 × 高 | 100 × 20 |
点击后 GuiButton_ChangeGUI.func_146113_a() 播放 gui.button.press 音效并打开 GuiMenuList 列表界面;
在列表里选中某项后 GuiMenuList.selectIndex() 调用 DummyConfig.setMainMenu(selected) 写回配置并保存。
4. 供其他 mod 使用的注册 API
MainMenuRegistry.registerNewGui(Class<? extends GuiScreen> menu, String name, String description)
(MainMenuRegistry.java:37-47):若传入类实现了 IMainMenu 就加入 menuList 与 menuInfoLst,
否则通过 Notifier.notifyCustomMod("DummyCore", ...) 报错并拒绝注册。
guis/menuInfoLst 按相同下标一一对应,描述文本缺省为 No description provided by author :(。
getGuiDisplayed()(MainMenuRegistry.java:64-74)供其它 mod 在绘制标题画面背景时取当前生效的菜单实例。
数值
| 数值 | 值 |
|---|---|
| 切换按钮 ID | 65535 |
默认菜单下标 mainMenuID |
0([DC] Vanilla) |
| 按钮宽度 × 高度 | 100 × 20 |
源码缺陷:两处 off-by-one 越界
菜单下标同时被两处边界检查使用,两处都写成了 <= / <,越界时抛 IndexOutOfBoundsException。
缺陷 1:MainMenuRegistry.java:51
if (menuList.size() < index) {
index = menuList.size() - 1;
}
currentScreen = menuList.get(DummyConfig.getMainMenu()).getConstructor().newInstance();
当 index == menuList.size() 时,menuList.size() < index 为 false,钳制不生效,
紧接着 menuList.get(index) 越界。正确写法应为 menuList.size() <= index。
由于外层有 try/catch (Exception),后果是只 printStackTrace() 然后 return,表现为主菜单静默打不开。
缺陷 2:GuiMenuList.java:95-96
if (var1 >= 0 && var1 <= MainMenuRegistry.menuList.size()) {
this.selectedMod = MainMenuRegistry.menuList.get(selected);
同样以 <= size() 作为合法上界,var1 == size 时直接 get() 越界,且此处没有 try/catch,
会直接抛到 GUI 渲染调用栈上。正确写法应为 var1 < MainMenuRegistry.menuList.size()。
另外 selectIndex 在 DummyConfig.setMainMenu(selected)(GuiMenuList.java:100)时无条件写配置,
即使 selected 是越界或非法值也会先落盘。
相关条目
- DummyCore 配置 —
mainMenuID/hideSwitchMenuButton默认值,以及每次开主菜单重写配置文件的缺陷