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():

  1. 若 category == null 直接返回 null;
  2. 测试模式且 URL 以 mock:// 开头时走 MockHelper.downloadMockText,否则 url.openStream() 读 UTF-8 文本;
  3. 解析 JSON,从 homepage(字符串)或 homepages(对象,键为显示名)里收集超链接,两个键同时存在或都不存在时报 "Failed to locate homepage(s) in ...";
  4. 取 promos 对象;不是对象则报 "Failed to locate promos in ...";
  5. 在 promos 的键里取 key.split("-")[0] 转 ComparableVersion,筛出 <= category.version 的,取其中最大者作为 newestLowerCategoryVersion;
  6. 若 newestLowerCategoryVersion == category.version 或 category.backwardsCompatible,才去查 promos[<newestLowerCategoryVersion>-<promoChannel>];
  7. 该 promo 不存在时报 "No promo named <key> found in <url>";因为不兼容而被跳过时报 "No promo found for non-backwards compatible category of version ... in ...";
  8. 一个可用 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 的资源包、自定义分类、向后兼容分类。

相关条目