主菜单替换系统

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 默认值,以及每次开主菜单重写配置文件的缺陷