UpdateCheck
从 mod 官方的 update JSON 接口拉取版本信息,比对当前版本,把"有更新"的组件汇总成一个 HTML 页面,并在主菜单放一个按钮。宿主 mod 提交一次任务即可。
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 共享模块 / 更新检查 |
| 入口 | MCLibModules.updateCheckAPI(UpdateCheckAPI 的共享实例) |
| MODID 常量 | UpdateCheckLib.MODID = "UpdateCheckLib" |
| 结果文件 | Launch.minecraftHome/updates.html |
| 配置文件 | 有:config/UpdateCheckLib.cfg,见 配置文件 |
| 提交时限 | post-init 阶段开始之前 |
| 网络协议 | 普通 HTTPS / 文件 JSON 拉取,不是 Forge SimpleNetworkWrapper 消息 |
UpdateCheckAPI
| 成员 | 签名 | 说明 |
|---|---|---|
| 预置分类 ID | public static String MODS_CATEGORY_ID = "mods" |
预置分类,依赖版本为 Loader.MC_VERSION,显示名 "Mod",不向后兼容 |
| 预置分类 ID | public static String RESOURCE_PACKS_CATEGORY_ID = "resource_packs" |
预置分类,显示名 "Resource pack",不向后兼容 |
| 便捷提交 | public void submitModTask(String modid, String updateJSONUrl) |
版本从 mod 的 @Mod 注解取 |
| 便捷提交 | public void submitModTask(String modid, String currentVersion, String updateJSONUrl) |
指定版本;Loader.instance().getIndexedModList().get(modid) 为 null 时 LOGGER.warn("Tried to register update check for non-existent modid: ...") 并返回 |
| 通用提交 | public void submitTask(String name, String currentVersion, String categoryID, String updateJSONUrl) |
name 是结果 UI 里的显示名;分类不存在时 LOGGER.warn("Tried to register a non-existent category for mod ...") |
| 注册自定义分类 | public void registerCategory(String id, String version, String displayName, boolean backwardsCompatible) |
ID 已存在则不做事 |
所有方法先 isEnabled() 判断,为假时静默返回。
关于 submitModTask 与 @Mod 注解:submitModTask 本身不读 @Mod 注解的 version 字段,而是通过 Loader.instance().getIndexedModList().get(modid) 拿 ModContainer 再取 mc.getVersion()。所以宿主 mod 必须是一个真正注册了的 mod——这正是 MCLib 自己不能用这套 API 检查自己的原因。
分类语义
UpdateCheckLib.UpdateCategory(包私有 static 类)字段:public String id、public String displayName、public String version、public boolean backwardsCompatible、public List<UpdateCheckTask.Result> results。
version 是组件依赖的版本号(mod / 资源包通常是 Minecraft 版本)。backwardsCompatible 表示"依赖版本比要求的高时组件还能不能用"——Forge mod 为 false(低版本 MC 的 mod 跑不了),而 MAtmos 声音包这类为 true(低版本做的包在高版本仍能用)。
排序 compareTo:预置的 mods 分类永远排最前,其余按 displayName 字母序。
版本求解算法
UpdateCheckTask.solveVersion():
- 若
category == null直接返回 null; - 测试模式且 URL 以
mock://开头时走MockHelper.downloadMockText,否则url.openStream()读 UTF-8 文本; - 解析 JSON,从
homepage(字符串)或homepages(对象,键为显示名)里收集超链接,两个键同时存在或都不存在时报"Failed to locate homepage(s) in ..."; - 取
promos对象;不是对象则报"Failed to locate promos in ..."; - 在
promos的键里取key.split("-")[0]转ComparableVersion,筛出<= category.version的,取其中最大者作为newestLowerCategoryVersion; - 若
newestLowerCategoryVersion == category.version或category.backwardsCompatible,才去查promos[<newestLowerCategoryVersion>-<promoChannel>]; - 该 promo 不存在时报
"No promo named <key> found in <url>";因为不兼容而被跳过时报"No promo found for non-backwards compatible category of version ... in ..."; - 一个可用 promo 都没有时抛
NoSuchElementException,捕获后报"No promo found for category version lower than <version> in <url>"。
promoChannel 取自配置,默认 "latest"。Forge 约定 latest 是开发版通道、recommended 是稳定版通道。
结果类型
UpdateCheckTask.Result(public static):
| 成员 | 说明 |
|---|---|
public ComparableVersion newVersion |
求解出的新版本;出错或无更新时为 null |
foundUpdate() |
newVersion != null && newVersion.compareTo(task.currentVersion) > 0 |
isInteresting() |
`(!ConfigUCL.hideErrored && newVersion == null) |
UpdateCheckTask.Hyperlink(public static)字段 final String url、final String display;单参构造把 display 也设为 url。
日志级别控制
UpdateCheckTask.getErrorLevel():配置 hideErrored 为假时 Level.ERROR,为真时 Level.DEBUG。同一个方法被七个失败分支共用,所以关掉 hideErrored 同时会把错误信息从 ERROR 降到 DEBUG。
结果渲染
ResultHTMLRenderer.render(File outFile):
- 若所有分类都没有
isInteresting()的结果,直接outFile.delete()(不生成空页面);否则从 classpath 读resources/mclib/v0_3_7/updatecheck/updates.template.html,把{table}占位符替换为生成的表格,UTF-8 写出。 - 表格顺序:
MODS、RESOURCE_PACKS在前,其余自定义分类按UpdateCategory排序追加。 - 列标题固定为
Name/Installed version/Latest version/Update link(常量FIELD_NAME等),表题为<显示名> updates。 - 无新版本的那一格写
<b>ERROR</b>。 - 多个超链接用
|(TABLE_HOMEPAGE_SEPARATOR)连接,格式为<a href="...">...</a>。 - 文本经
StringEscapeUtils.escapeHtml4转义;URL 用URLEncoder后把%3A还原成:、%2F还原成/。
线程模型
UpdateCheckLib 用 new ThreadPoolExecutor(0, 4, 60, TimeUnit.SECONDS, workQueue),最多 4 个并发下载。postInit 里 CompletableFuture.allOf 汇总后统计 updateCount,调用 onFinished() 写 HTML,客户端再 onFinishedClient() 更新按钮数字。
MockHelper
包路径 makamys.mclib.updatecheck。用一个假"网络"替代真实下载,供自动化测试使用。
| 成员 | 值 / 说明 |
|---|---|
| 测试开关 | 系统属性 updateCheckLib.test,默认 false |
| URL 前缀 | public static final String MOCK_PREFIX = "mock://" |
| 属性键前缀 | updatechecklib_mock_url_(私有) |
isTestMode() / isMockUrl(String) |
前缀判断 |
downloadMockText(String url) |
读 System.getProperty("updatechecklib_mock_url_" + url);URL 非 mock 开头抛 IllegalArgumentException |
uploadMockText(String url, String text) |
System.setProperty 写入并返回 url |
注意 updateCheckLib.test 为假但 URL 用了 mock:// 时,会走真实 new URL(...) 分支并失败。
完整使用示例(源码内 updatecheck/test/UCLTest.java)
示例 mod 覆盖了:真实网络检查、mock 的"过期/最新/过新"三种 mod、多超链接、版本号写成 @VERSION@、名称里塞 HTML 的 XSS 尝试、坏 URL 的资源包、自定义分类、向后兼容分类。
相关条目
- 配置文件 -
UpdateCheckLib.cfg的七个选项 - GUI 注入 - 主菜单按钮与
Util工具 - MCLib 核心与任务队列 -
MCLibModules.updateCheckAPI的取得 - SSL 信任库替换 - 结果页模板所在目录