unsup 中文教程 - SJMC-Dev/SMP3-modpack GitHub Wiki

unsup 中文手册

unsup 是一个 Java 写的「通用工作目录同步器」,最常见的用途是做 Minecraft 整合包的更新器:它把一份远端的「清单」和你的工作目录(.minecraft 或服务器根目录)对比,把缺的、旧的、被改过的文件补齐,让目录和作者发布的状态保持一致。

本文按读者身份分章,不必从头读到尾:

你是谁 读哪几章
玩家(装包、玩) 第 1 章 + 第 3 章
服主(开服) 第 1 章 + 第 4 章
包作者(发包 + 自测) 第 1、2、5、6 章
出问题了 第 7 章 + 第 8 章附录

关于本文的来源与可信度

  • 本文所有行为结论来自对本仓库源码的只读阅读,关键处标注了 文件名:行号,可自行核对。
  • 未验证项(文末第 8.6 节也列了):
    • 没有在真实的 Minecraft 客户端 / 服务端 / Prism / MultiMC 实例上运行过 unsup;
    • 没有执行过 ./gradlew build,构建相关表述只来自 build.gradle 的静态阅读;
    • 本机 DNS 解析不了 git.sleeping.town,官方 wiki(Config format / Manifest format)原文未读到,凡涉及 wiki 的内容标为**〈搜索摘要〉**;
    • packwiz 侧行为(如 packwiz modrinth add --side、.packwizignore 是否被 refresh 尊重)来自**〈搜索摘要〉**,未在本机验证。

第 1 章 速查一页

1.1 三行结论

  1. 你是玩家/服主?只需要两个文件:unsup.jar + unsup.ini,放在工作目录里。
  2. unsup.ini 只回答一个问题:从哪个地址同步哪个包 —— 也就是 source_format= 和 source= 这两行,其余都是可选项。
  3. pack.toml、index.toml、.pw.toml、unsup.toml、manifest.json、flavor、env 这些东西都在「包」里,是包作者的地盘,本地一般不用碰。

1.2 全景图:东西都在哪

远端 = 包的仓库(包作者维护)
  pack.toml              包的入口(packwiz 格式):游戏版本、索引位置
  index.toml             文件清单:有哪些文件、每个文件的 hash
  mods/xxx.pw.toml       单个文件的「卡片」:从哪下、hash 多少、side 是什么
  unsup.toml             可选的「unsup 专属设置」:flavor 组、加速开关
  manifest.json          不用 packwiz 时的另一套格式(native)
  blobs/… raw/… CDN …    真正的 jar、zip 文件

本地 = 你的机器(你维护)
  unsup.jar              程序本体
  unsup.ini              你唯一的配置文件
  .unsup-state.json      unsup 的账本:同步到哪个版本、口味选了什么
  unsup.log              日志(debug 内容始终写入)
  .unsup-tmp/            同步过程中的暂存目录(下载完会清理)
  mods/ shaderpacks/…    同步下来的真实文件

关键:本地永远不会出现 .pw.toml 这种「卡片」——unsup 只会把卡片指向的真实文件下载到工作目录。所以你在本地找不到它们,也就不用管它们。

1.3 四个启动命令

命令 谁调用 跑完之后 界面
java -jar unsup.jar 你手动运行 同步完退出,不启动任何程序 默认不显示
java -jar unsup.jar server 你手动运行,并指定环境 server 同上 同上
-javaagent:unsup.jar JVM 在启动游戏前调用 把控制权交还给游戏,游戏继续启动 默认显示
-javaagent:unsup.jar=server 同上,并指定环境 server 同上 同上
  • java -jar 叫 standalone(独立运行);-javaagent 叫 agent(挂载启动)。
  • 等号后面的 server 不是命令,而是 unsup.ini 里定义的一个环境名(见 1.4)。
  • 依据:Bootstrap.java:26-35(两个入口分别调 Agent.main / Agent.premain)、Agent.java:87-92。

1.4 两个筛选维度:env 与 flavor

谁决定 作用 一句话
env(环境) 机器自动判定(agent 模式)或命令行指定(standalone) 排除「这台机器根本不该有的文件」,例如服务端不要光影 「我这台是客户端还是服务端」
flavor(口味) 用户在对话框里选,结果记进 .unsup-state.json 在互不兼容的组合里选一套,例如「要不要装性能 mod」 「我要哪一套搭配」

重要的两条:

  • 这两个规则都写在包里(由作者标在文件上),不是写在你的 unsup.ini 里。
  • 规则逐个文件生效,unsup 没有「整个目录排除」的写法。

所以「让服务端不要下 shaderpacks/」这类需求,必须由包作者在包里标(见 5.4、5.6);服主在本地配不出来。

1.5 可以放心忽略的名词

versions.unsup 该填什么、metafiles_zip、puppet_mode、alt_public_key、server_authority、enforce_secure_hashes 这类,除非你开始发自己的包或排错,都用不到。


第 2 章 全景与术语

本章解决:把名词对上号。只想用起来可以跳过,遇到不懂的词再回来查。

2.1 一次同步发生了什么

  1. 读工作目录的 unsup.ini;读不到就什么都不做(Agent.java:236-307,日志 No config file found? Doing nothing.)。

  2. 按 source_format 选择处理方式,去 source 拉清单(UpdateHandler.java:107-113):

    source_format 拉的东西 处理类
    packwiz pack.toml PackwizHandler
    unsup 原生 manifest.json NativeHandler
    raw 单个文件(调试用) RawHandler
  3. 比对本地的 .unsup-state.json 与远端版本,生成更新计划(要下哪些、要删哪些)。

  4. 需要用户点头时(看 behavior、有没有版本更新、有没有文件冲突)弹对话框。

  5. 下载到 .unsup-tmp/,逐个校验 hash;校验通过才在提交阶段落盘。

  6. 写回 .unsup-state.json,输出 Update successful!(UpdateHandler.java:454)。

  7. agent 模式把控制权交还游戏;standalone 模式直接结束。

2.2 术语表

名词 一句话解释
workdir(工作目录) unsup 同步的目标目录。客户端通常是 .minecraft,服务端通常是服务器根目录。JVM 的当前目录决定它是谁
manifest(清单) 「应该有哪些文件、各是什么 hash」的描述文件。packwiz 用 pack.toml+index.toml,native 用 manifest.json
metafile(.pw.toml) packwiz 里「一个文件的卡片」:下载地址、hash、side、可选项
side 卡片上的「客户端 / 服务端」标记:client / server / both
env(环境) unsup.ini 的 [env.<名>] 定义,如 client、server;server 就是其中一种名字
flavor(口味) 一组互斥的选项,用户选择后决定哪些文件会被同步
preset 内置配置预设,preset=minecraft 会带来 env、recognize_nogui、组件映射等默认值
puppet 界面进程。有 Swing 版和 OpenGL(SDL3) 版,puppet_mode 可强制选择
signature(签名) 用 signify / ed25519 / sigsum 给清单或引导配置签名,防止被篡改

2.3 文件与格式对照

文件 格式 在哪 谁维护
unsup.ini INI 本地工作目录 你 / 包作者随包分发
unsup.toml TOML 远端,与 pack.toml 同目录 包作者(可选)
pack.toml / index.toml / *.pw.toml TOML 远端 包作者(packwiz 生成)
manifest.json / versions/N.json / bootstrap.json JSON 远端 包作者(native 格式)
.unsup-state.json JSON 本地 unsup 自动维护
unsup.log 文本 本地 unsup 自动写

第 3 章 玩家篇

本章解决:怎么把包装上、怎么更新、弹窗怎么处理。服主请跳到第 4 章。

3.1 方式 A:任何启动器 + JVM 参数(通用)

  1. 下载 unsup-<版本>.jar(发布页同时提供 .sig 签名文件和 Prism 用的 diy.y2k.unsup.json)。

  2. 把 unsup.jar 和整合包作者提供的 unsup.ini 都放进游戏的工作目录(原版启动器就是 .minecraft)。

  3. 在启动器的 JVM 参数里加:

    -javaagent:unsup.jar
    
  4. 正常点「开始游戏」。unsup 会先同步、再让游戏启动;已经是最新版就什么都不显示。

也可以强制指定环境(等号后面跟环境名):

-javaagent:unsup.jar=server

路径相对工作目录解析,写绝对路径也行。agent 参数必须排在 -jar 之前。

3.2 方式 B:Prism Launcher 用「组件(component)」

  1. 从最新 release 下载 diy.y2k.unsup.json,在 Prism Launcher 里作为 component 导入。
  2. 之后 Prism 会下载并管理 unsup,unsup 自己也能被更新。
  3. 包里如果声明了组件版本(pack.toml 的 [versions] + unsup.ini 的 [mmc-component-map]),还能顺带更新 mod loader / Minecraft 本体。组件被更新后 unsup 会退出并提示重启(见 4.6)。
  4. MultiMC 不支持 java agent 形式的组件。

3.3 首次同步 vs 静默通过

  • 首次同步(本地没有 .unsup-state.json):会下载整个包,界面显示 Bootstrapping,耗时较长。
  • 已是最新:日志一句 We appear to be up-to-date. Nothing to do(PackwizHandler.java:613),界面不弹出来,游戏直接启动。
  • 完全没反应:多半是工作目录里没有 unsup.ini(Cannot find a config file, giving up.),unsup 会让游戏照常启动。

3.4 界面什么时候出现

默认策略是「只在该问你的时候才出现」:

  • 有更新要确认(behavior=manual 时)→ 弹更新对话框;
  • 有文件冲突 → 弹冲突对话框;
  • 有没选过的 flavor 组 → 弹口味选择对话框;
  • 一切正常且无事可问 → 不显示窗口(源码为此设了 1250ms 延迟再显示,避免闪一下)。

3.5 冲突、备份与 .orig

冲突的含义是:你本地改过的文件,正好这次也要被上游更新(UpdateHandler.java:187-271)。分四类:

类型 含义
LOCAL_AND_REMOTE_CHANGED 本地改了,远端也改了
LOCAL_AND_REMOTE_CREATED 本地新加的文件,远端也新增了同名文件
LOCAL_CHANGED_REMOTE_DELETED 本地改了,远端删了
LOCAL_DELETED_REMOTE_CHANGED 本地删了,远端改了

处理方式:

  • 对话框有 是 / 否 / 全部是 / 全部否 / 取消;「全部」会把同类冲突一并处理。
  • 选「否」= 跳过这个文件(保留你的版本)。
  • 选「取消」= 直接退出,什么都不做(退出码 4,USER_REQUEST)。
  • 选「是」且原文件存在时,原文件会被改名成 <文件名>.orig 保留下来,不会被直接毁掉。
  • behavior=auto 时不询问,全部按「是」处理。

3.6 玩家常见问题

现象 原因 / 处理
点了开始,游戏正常起,但没更新 工作目录没有 unsup.ini,或本来就是最新
每次启动都提示有更新 包源在反复变动,或本地文件被别的工具改过(看 unsup.log 里的 hash)
弹冲突,不知道选什么 想保留自己的改动就选「否」;想跟随上游就选「是」(原文件会留 .orig)
界面起不来(Linux/Wayland) 让包作者开 no_gui=true,或加 -Dunsup.puppetMode=swing 试试
下载很慢 -Dunsup.downloadWorkers=1 调小并发;国内网络问题见 7.5
想手动跑一次看计划 java -Dunsup.dryRun=true -jar unsup.jar(不动盘,只打印计划)

第 4 章 服主篇

本章解决:服务器怎么同步、怎么不弹窗、退出码什么意思。玩家请回第 3 章。

4.1 standalone 用法

# 在服务器根目录(unsup.ini 所在处)
java -jar unsup.jar                 # 不带 env 的配置
java -jar unsup.jar server          # 带 use_envs 的配置必须给环境名
  • standalone 模式下 unsup 同步完就退出,不会启动服务端(Agent.java:187-188)。
  • standalone 默认不显示界面(Config.java:368-369:if (Agent.standalone) return !SysProps.GUI_IN_STANDALONE.orBias();)。
  • 想让 standalone 也显示界面,加 -Dunsup.guiInStandalone=true。
  • 注意:standalone 模式下 unsup.ini 里的 no_gui / recognize_nogui 不参与判断(默认就已经没界面了)。

4.2 服务端挂 agent(推荐给开服脚本)

java -Xmx4G -javaagent:unsup.jar -jar server.jar nogui
  • -javaagent 是 JVM 特性,服务端同样适用;agent 参数要写在 -jar 之前。
  • 常见服务端启动脚本自带 nogui 参数,配合内置 preset 的 recognize_nogui=true 会自动不显示界面(Config.java:371-377,按 sun.java.command 整词匹配 nogui / --nogui)。
  • 工作目录必须一致:unsup.ini、unsup.jar、.unsup-state.json 都按 JVM 当前目录解析。别在 A 目录跑 standalone、在 B 目录跑 agent,否则账本对不上。
  • Forge / NeoForge 的现代启动脚本可以把 -javaagent:unsup.jar 写进 user_jvm_args.txt,不用改 run.sh。
  • update_mmc_pack=true(内置 minecraft preset 默认开启)在纯服务端上没有副作用:找不到 ../mmc-pack.json 时只输出一行 update_mmc_pack is enabled, but I don't see an mmc-pack.json. Ignoring(MMCUpdater.java:48-52)。

4.3 环境(env)是怎么判定的

优先级从高到低(Config.java:238-294):

  1. 命令行给的:java -jar unsup.jar server 或 -javaagent:unsup.jar=server。
  2. unsup.ini 的 force_env=。
  3. 自动探测(仅 agent 模式):遍历 [env.<名>] 的 marker=,用 classloader 找这些资源——
    • marker=* 表示「以上都不匹配就归我」;
    • 不带 / 的 marker 会被当成类名(. → / 并补 .class),例如 net.minecraft.client.main.Main。

use_envs=true 时,如果判定不出环境,或给了不在合法名单里的环境名,unsup 会列出有效值并以退出码 1 退出。

  • 内置 minecraft preset 的 [env.server] marker=* 就是服务端的兜底:服务端 classpath 里没有客户端类,于是自动算 server。
  • 想忽略环境、所有文件都下:-Dunsup.ignoreEnvs=true(SysProps.IGNORE_ENVS)。

4.4 关掉界面:no_gui / recognize_nogui / puppetMode

agent 模式下的判定顺序(Config.java:368-378):

if (Agent.standalone) return !SysProps.GUI_IN_STANDALONE.orBias();  // standalone:只看这个 sysprop
if (ini.containsKey("no_gui")) return ini.getBoolean("no_gui", false); // ini 显式写了就以它为准
if (ini.getBoolean("recognize_nogui", false)) { ...检查 nogui 整词... }
  • 想彻底关界面:no_gui=true。
  • 界面异常时还可以指定实现:-Dunsup.puppetMode=swing 或 opengl。
  • 界面进程崩溃时默认不会中断同步(-Dunsup.abortOnPuppetCrash 默认 false,仅供调试界面时设为 true)。

4.5 退出码

退出码 枚举 含义
0 SUCCESS 成功
1 CONFIG_ERROR 配置错误(缺必填项、格式错误、env 判定失败等)
2 CONSISTENCY_ERROR 一致性 / 状态文件错误(.unsup-state.json 读不了)
3 BUG 内部错误
4 USER_REQUEST 用户取消(关窗口、点取消)
5 BOOTSTRAP_FAILED 引导配置(unsup.bootstrapUrl)下载失败

来源:ExitCode.java。

4.6 组件更新后要重启

如果启用了组件同步(update_mmc_pack=true + [mmc-component-map]),unsup 会改写 ../mmc-pack.json 与 ../patches/*(注意是工作目录的父目录,即 Prism/MultiMC 的实例根)。MMCUpdater 用 *.unsupnew 临时文件再替换,源码注释解释了 MultiMC 会在 5 秒后回写该文件,所以:

  • 组件有变化时 unsup 会应用更新并直接退出,日志 A component update has been applied - exiting for game restart.(Agent.java:184-186),退出码 0;
  • 必须重启游戏才会真正生效。

4.7 服主常见问题

现象 原因 / 处理
Cannot sync an env-based config in standalone mode unless an argument is given 配置用了 use_envs,standalone 下必须给环境名(java -jar unsup.jar server)或写 force_env=
服务器上弹窗失败/异常 用 no_gui=true;界面崩溃本身不会中断同步
想让服务端不下光影 由包作者在包里标 side = "client"(见 5.4),本地无解
同步后自己改过的文件被冲掉 那是包管理范围内的文件;冲突时会留 .orig
想先看看会改什么 java -Dunsup.dryRun=true -jar unsup.jar server,配合 -Dunsup.debug=true 更详细

第 5 章 包作者篇

本章解决:包怎么写、文件怎么标 side 与 flavor、unsup.toml 怎么用、签名怎么弄。纯玩家/服主可以跳过。

5.1 packwiz 包的结构

pack.toml                 入口:pack-format、版本、[index]、[versions]
index.toml                文件清单(由 packwiz refresh 生成)
mods/sodium.pw.toml       每个文件的「卡片」(metafile)
shaderpacks/xxx.pw.toml   同上
unsup.toml                可选:unsup 专属扩展(flavor 组等)

unsup 读取 pack.toml 时会检查 pack-format:必须是 unsup-packwiz,或者 packwiz: 且版本 ≥ packwiz:1.1.0(PackwizHandler.java:89-93)。

5.2 pack.toml:入口

pack-format = "packwiz:1.1.3"
name = "My Pack"
version = "1.0"

[index]
file = "index.toml"
hash-format = "sha256"
hash = "…"           # index.toml 自身的 hash,packwiz refresh 会写

[versions]
minecraft = "1.21.1"
fabric = "0.16.5"
unsup = "1.3"        # 存在这个键才会去读 unsup.toml(见 5.7)

[index] 是必需的,缺失会报 Malformed pack.toml: [index] table is missing。unsup 用 hash-format:hash 这个「双元素」判断索引有没有变化(对比状态里的 lastIndexHash),以此决定要不要更新。

5.3 index.toml 与 .pw.toml

index.toml 里每一项分两类,这个区别非常重要:

hash-format = "sha256"

# ① metafile 条目:有 metafile = true,真正的信息在 .pw.toml 里
[[files]]
file = "mods/sodium.pw.toml"
hash = "…"
metafile = true

# ② 普通文件条目:文件本身就在包里,unsup 直接下载它
[[files]]
file = "shaderpacks/MyShader.zip"
hash = "…"
alias = "shaderpacks/MyShader.zip"
metafile 条目 普通文件条目
有 .pw.toml 卡片 有 无
支持 side(客户端/服务端过滤) 支持 不支持
支持 [metafile.x] flavors 口味绑定 支持 不支持
download.mode / CurseForge metadata 支持 不支持

源码依据:PackwizHandler.java:349-401 —— 普通条目分支里没有任何 side / flavors / env 判断,直接加入下载计划。

.pw.toml(metafile)的结构:

name = "Sodium"
filename = "sodium-fabric-0.6.0.jar"
side = "client"                                   # client / server / both
download = { url = "https://…", hash-format = "sha256", hash = "…" }

[option]                                          # 可选:把它变成 On/Off 开关
optional = true
default = false

hash-format 支持 md5 / sha1 / sha256 / sha384 / sha512 / murmur2(murmur2 是 CurseForge 的,unsup 会先转成十六进制;PackwizHandler.java:640-650)。

5.4 side:客户端 / 服务端

在 metafile 里写 side = "client",unsup 就会在 env 不符的机器上跳过它(PackwizHandler.java:426-432):

String side = parseSide(metafile.getString("side"));
if (side != null && Agent.config().useEnvs() && !side.equals(Agent.config().detectedEnv())) {
    Log.info("Skipping "+path+" as it's not eligible for env "+…+" (wants "+side+")");
    continue;
}

生效前提:

  1. use_envs=true(preset=minecraft 已默认开启);
  2. 那台机器确实被判成目标环境(服务端通常是 server,见 4.3);
  3. 该文件是 metafile 条目(普通条目不支持 side)。

服务端效果示例(这就是「服务端不下光影」的正解):

# shaderpacks/complementary-reimagined.pw.toml
name = "Complementary Reimagined"
filename = "ComplementaryReimagined_r5.5.1.zip"
side = "client"
download = { url = "…", hash-format = "sha256", hash = "…" }

改完要 packwiz refresh 并重新发布。副作用:生效后的第一次同步,客户端/服务端上已有的这些文件会被 unsup 删掉(它们不在新计划里,UpdateHandler.java:582-587 的删除分支)。不想被删就先备份。

如果文件是「普通条目」,只有两条路:把它改成 metafile(需要一个可下载的 URL),或者让它根本不进 index(.packwizignore,见 5.10)。

5.5 unsup.toml:包侧的 unsup 扩展

位置与启用条件:

  • 必须放在 pack.toml 同目录,文件名固定 unsup.toml;
  • 只有 pack.toml 的 [versions] 里有 unsup 这个键时才会被读取(PackwizHandler.java:203-208)。

支持三块内容:

① [flavor_groups.<组id>] —— 定义口味组

[flavor_groups.shaders]
name = "Shader Pack"            # 可选,缺省用组 id
description = "要不要装光影"     # 可选,缺省是「无描述」
side = "client"                 # 可选:client / server / both / 空

[[flavor_groups.shaders.choices]]
id = "shaders_on"
name = "装"
description = "性能会下降"

[[flavor_groups.shaders.choices]]
id = "shaders_off"
name = "不装"
  • 认的字段只有这些:组上是 name / description / side;选项上是 id / name / description(PackwizHandler.java:210-262)。
  • 选项也可偷懒写成纯字符串数组:choices = ["shaders_on", "shaders_off"]。
  • 没有 default 字段:默认值只能写在客户端 unsup.ini 的 [flavors] 段里(或 -Dunsup.flavor.<组>=<选项>)。
  • side 与当前 env 不符时,整个组不出现、也不询问。
  • 组保持两个选项、且 id 形如 <组id>_on / <组id>_off 时,unsup 会把它渲染成布尔开关(FlavorGroup.java:34-44)。

② [metafile.<名字 或 /路径>] —— 把 metafile 绑到口味

[metafile.complementary-reimagined]
flavors = "shaders_on"                     # 单值,或写成数组

[metafile.another-thing]
flavors = ["shaders_on", "lux_on"]

语义是交集:文件上列的选项里只要有一个被选中,该文件就同步,否则跳过(PackwizHandler.java:519-530)。键可以写 metafile 名字,也可以写 /shaderpacks/xxx.pw.toml 这样的路径(源码里两个都查)。

③ features.metafiles_zip —— 加速开关

features.metafiles_zip = true

开启后 unsup 会下载 unsup-metafiles.zip.br(brotli 压缩的 zip,里面是所有 .pw.toml),把「N 个 metafile = N 次请求」压成 1 次;每个条目仍会重新校验 hash,条目缺失或 hash 不符只警告并回退逐个下载(PackwizHandler.java:279-299、360-386)。它是可选的分发物,由作者/CI 生成,没有它 unsup 照常工作,只是慢一点。

5.6 flavor 的三种标记方式

前提:flavor 的粒度是单个文件,标签内容是选项 id,没有目录/通配符写法;packwiz 模式下只有 metafile 能被标。

方式 1:packwiz 原生 option.optional(最省事)

[option]
optional = true
default = false

unsup 会自动合成一个 On/Off 口味组:组 id = metafile 名字,选项 id = <名字>_on / <名字>_off,默认值取 option.default,并自动把该 metafile 绑定到 _on(PackwizHandler.java:444-475)。如果你已经在 unsup.toml 里显式配了这个 metafile 的 flavors,显式配置优先,不再生成合成组。

方式 2:unsup.toml 显式定义组 + 绑定(要控制力时用)

见 5.5 的 ① 与 ②,再加上客户端默认值:

# 客户端 unsup.ini
[flavors]
shaders=shaders_on

方式 3:原生 manifest 的 flavors 字段(唯一能覆盖「普通文件」的)

原生 manifest 里每个文件条目都自带 flavors,连普通文件也能标:

{
  "path": "shaderpacks/MyShader.zip",
  "hash": "…",
  "size": 12345,
  "url": "https://…",
  "envs": ["client"],
  "flavors": ["shaders_on"]
}

代价是放弃 packwiz 生态(要自己生成 manifest 与 versions/N.json 的增量链)。

5.7 versions.unsup 的两个作用

[versions]
unsup = "1.3"
  1. 开关:判定条件是 containsPrimitive("unsup") —— 只要键在,unsup 就会去读 unsup.toml,值写什么都一样;读不到只打一条 debug 日志 unsup is in the versions table, but there's no unsup.toml。
  2. unsup 自己作为 component 的目标版本:[versions] 整张表会与 Prism/MultiMC 的 components 比对(通过 unsup.ini 的 [mmc-component-map] 把 unsup 映射到 com.unascribed.sup / com.unascribed.unsup / diy.y2k.unsup)。比对是字符串精确相等(!ours.equals(...),PackwizHandler.java:119-124),所以只在你把 unsup 当组件管理时才需要写准;写 1.3 而实际装的是 1.3.0 会导致每次都判定「有组件更新」→ 更新后退出重启。

5.8 签名(public_key 与 .sig)

unsup.ini:

public_key=signify RWxxxx…      # 主钥
alt_public_key=ed25519 MIIB…    # 备用钥,可选
  • 语法是 "<kind> <base64>",kind 只能是 ed25519、signify、sigsum(SigProvider.java:34-42,其他值报 Unknown key kind …)。

  • signify 用 OpenBSD 格式公钥(base64 解码后以 Ed 开头 + 8 字节 keyId + Ed25519 公钥);对应的 .sig 是 signify 文本格式(第二行是 base64 签名),keyId 不符会报 Data is signed with the wrong key(SignifyProvider.java:55-87)。

  • ed25519 是 X.509 SubjectPublicKeyInfo 公钥 + 64 字节裸签名。

  • 生成签名(本仓库自己的做法,见 build.gradle 的 sign 任务):

    signify -c "verify with keys/release.pub" -S -s <私钥> -m <文件> -x <文件>.sig
    
  • 需要签名的地方:

    • packwiz 模式:pack.toml 旁边的 unsup.sig;
    • native 模式:manifest.sig、versions/N.sig、bootstrap.sig;
    • unsup.toml 本身不参与签名校验(加载它时签名参数是 null)。
  • 不用签名也不算错,但 source 是明文 http 时 unsup 会警告 Using unencrypted HTTP without manifest signing - this is a very bad idea!(本地回环地址不触发;UpdateHandler.java:100-106)。

5.9 原生 manifest(native)对照

source_format=unsup 时用的是自己写的 JSON:

{
  "unsup_manifest": "unsup-1",
  "versions": { "current": { "name": "1.0", "code": 1 } },
  "flavor_groups": [
    {
      "id": "shaders",
      "name": "Shader Pack",
      "description": "要不要装光影",
      "envs": ["client"],
      "choices": [
        { "id": "shaders_on", "name": "装" },
        "shaders_off"
      ]
    }
  ]
}

要点(NativeHandler.java:64-175):

  • 根清单、bootstrap.json、versions/N.json 各自带一个 unsup_manifest 标识(root-1 / bootstrap-1 / update-1),版本号不认识会拒绝解析。
  • 版本是递增的 code:本地 current_version 落后几个版本,就依次拉 versions/<code>.json 应用增量;bootstrap.json 是首次安装时的快照。
  • 文件条目字段:path / hash / size / url(省略则从 blobs/<hash前两位>/<hash> 取)/ envs(数组)/ flavors(数组)。
  • flavor_groups 用 envs 数组限定环境;只有老式 flavors 数组时,unsup 会合成一个 id 为 default、标题为 Flavor 的组,并把名字里括号内容当作描述(Forge (推荐) → 名字 Forge、描述 推荐)。
  • 旧状态里的单值 flavor 会自动迁移成 flavors 数组。

5.10 作者常见的坑

现象 原因
加了 unsup 键但 unsup.toml 没生效 文件不在 pack.toml 同目录,或文件名不对
标了 side 但服务端还是下载 那条是普通文件条目(index 里没有 metafile = true),或 use_envs 没开
标了 flavors 但没反应 同上(只对 metafile 生效),或该 metafile 已选了口味而没重新问(用 -Dunsup.packwiz.changeFlavors=true)
改了 .pw.toml 但客户端没更新 忘了 packwiz refresh(index 没变,unsup 就认为没更新)
无界面环境报 No choice provided for flavor group 该口味组没有默认值。给 [flavors] 组=选项,或把组标成 side = "client" 让它只在客户端出现
下载后 hash 不符,提示 Git LFS / 行尾 托管端把文件改坏了:git-lfs 指针文件、core.autocrlf 行尾转换、JS/CSS 压缩、图片优化(UpdateHandler.java:487-503)
想排除一整个目录 unsup 不支持目录级排除;只能把文件改成 metafile 逐个标,或用 .packwizignore 让它不进 index

.packwizignore 补充:packwiz 的 .packwizignore(语法同 .gitignore,放在 index.toml 旁边)会让 packwiz refresh 不把这些文件写进 index(〈搜索摘要〉)。文件不进 index,unsup 就永远不会同步它——但反过来,客户端也拿不到它,且已经在客户端/服务端的那些副本会在下一次同步时被删掉。


第 6 章 自测篇:作者以玩家身份验证

本章解决:包改完了,怎么在自己机器上确认「玩家拿到的是对的」。

6.1 准备一个最小可跑包

testpack/
  pack.toml
  index.toml
  mods/example-mod.pw.toml
  shaderpacks/example-shader.pw.toml
  unsup.toml

pack.toml:

pack-format = "packwiz:1.1.3"
name = "Test Pack"
version = "1.0"

[index]
file = "index.toml"
hash-format = "sha256"
hash = "<index.toml 的 sha256>"

[versions]
minecraft = "1.21.1"
unsup = "1.3"          # 想测 unsup.toml 就必须有这一行

index.toml:

hash-format = "sha256"

[[files]]
file = "mods/example-mod.pw.toml"
hash = "<该 .pw.toml 的 sha256>"
metafile = true

[[files]]
file = "shaderpacks/example-shader.pw.toml"
hash = "<该 .pw.toml 的 sha256>"
metafile = true

mods/example-mod.pw.toml:

name = "Example Mod"
filename = "example-mod.jar"
side = "both"
download = { url = "http://127.0.0.1:8000/files/example-mod.jar", hash-format = "sha256", hash = "<jar 的 sha256>" }

shaderpacks/example-shader.pw.toml:

name = "Example Shader"
filename = "example-shader.zip"
side = "client"        # 用它验证「服务端不下光影」
download = { url = "http://127.0.0.1:8000/files/example-shader.zip", hash-format = "sha256", hash = "<zip 的 sha256>" }

unsup.toml(可选,用来测口味):

[flavor_groups.shaders]
name = "Shader Pack"
side = "client"

[[flavor_groups.shaders.choices]]
id = "shaders_on"
name = "装"

[[flavor_groups.shaders.choices]]
id = "shaders_off"
name = "不装"

[metafile.example-shader]
flavors = "shaders_on"

hash 必须算对,否则 unsup 会报 hash 不匹配:

sha256sum index.toml                                   # Linux / macOS
shasum -a 256 index.toml
certutil -hashfile index.toml SHA256                   # Windows

更省事的做法:用 packwiz refresh 生成 index.toml 并自动写好 hash(packwiz 用法见其官方文档)。

6.2 本地起一个假包源

# 在 testpack 的父目录执行,把整个目录挂到 8000 端口
python3 -m http.server 8000        # Windows 上可能是 python -m http.server 8000

然后本地工作目录的 unsup.ini 指向它:

version=1
preset=minecraft
source_format=packwiz
source=http://127.0.0.1:8000/testpack/pack.toml

本地回环地址不会触发「明文 HTTP 没签名」的警告(UpdateHandler.java:100-106 排除了本机地址)。

6.3 dryRun:只看计划、不动盘

# Windows: java -Dunsup.dryRun=true -jar unsup.jar server
java -Dunsup.dryRun=true -jar unsup.jar server
  • dryRun 下不创建 .unsup-tmp、不写任何文件,只在日志里打印计划(UpdateHandler.java:272-275、384),结束时提示 Performed a dry run, per your request. No files in the working directory were changed!(Agent.java:181-183)。
  • 想看更多细节:-Dunsup.debug=true;想在应用前停下来看计划:-Dunsup.debug.pauseBeforeUpdate=true(会等 4 秒)。

6.4 日志断言清单

日志行 说明
Starting in standalone mode. unsup v… / Launch hijack successful. 两种模式各自的开场白(Agent.java:96)
Declared env is server / Detected env is server 前者=命令行/force_env 指定,后者=探测得到(Config.java:288-292)
Retrieving from … in PACKWIZ format 正在按 packwiz 处理(UpdateHandler.java:99)
Skipping … as it's not eligible for env server (wants client) side 过滤生效
Skipping … as it's not eligible for our selected flavors (wants any of …) flavor 过滤生效
We appear to be up-to-date. Nothing to do 已是最新
Update successful! 更新提交完成(UpdateHandler.java:454)
A component update has been applied - exiting for game restart. 组件更新了,需要重启

6.5 四场景矩阵

场景 怎么跑 期望
作者本机当客户端(agent) 启动器 JVM 加 -javaagent:unsup.jar 探测为 client,side="client" 与 side="server" 的文件按预期各自同步/跳过
作者本机当服务端(standalone) java -jar unsup.jar server 判定 server,side="client" 的文件被跳过;无界面
玩家实机 玩家按第 3 章操作 首次全量下载;再启动一次应「静默通过」
Prism 组件 导入 diy.y2k.unsup.json unsup 自身可被更新;若带组件版本,更新后提示重启

建议每轮至少跑一遍「同一份工作目录连续跑两次」:第一次应有下载,第二次应输出 Nothing to do;如果第二次仍在下载,说明包源在变或 hash 不稳定。

6.6 测完的清理

rm -rf .unsup-state.json .unsup-tmp unsup.log     # Windows: del / rmdir

.unsup-state.json 里记着已同步状态与口味选择;想重新体验「首次同步」就删掉它。


第 7 章 排错篇

本章解决:出问题了从哪看、怎么定位。所有排查都从 unsup.log 开始。

7.1 先看日志

  • 工作目录下的 unsup.log:debug 级内容始终写入,即使没开 -Dunsup.debug=true。
  • 想更详细:-Dunsup.debug=true、-Dunsup.debug.requests=true(打印每个请求)。
  • 想看更新计划而先不动盘:-Dunsup.dryRun=true。

7.2 常见症状对照表

症状 可能原因 处理
什么都不发生 工作目录没有 unsup.ini;或已是最新 确认 Cannot find a config file, giving up. / Nothing to do
退出码 1 配置错误:缺 version/source_format/source、version 不是 1、URL 非法、env 判定失败、口味无默认值 按日志里的具体提示改 unsup.ini
退出码 2 .unsup-state.json 读不了 备份后删除它,让 unsup 重新全量校验
退出码 5 unsup.bootstrapUrl 下载失败(含 .sig 校验失败) 检查 URL 可达性与 unsup.bootstrapKey
退出码 4 用户在对话框里点了取消 重新运行
下载报 hash 不匹配 托管端改坏了文件 看提示:Git LFS 指针、行尾转换、JS/CSS 压缩、图片优化
服务端每次都「有更新」 versions.unsup 与实例里组件的版本字符串不一致 改成完全一致的字符串,或不要开 update_mmc_pack
界面不出现 no_gui=true、识别到 nogui、或 standalone 模式 需要界面时用 -Dunsup.guiInStandalone=true;agent 模式检查 no_gui 设置

7.3 「标记了 side / flavors 却不生效」排查流程

  1. 看 index:这条是 metafile 吗?(metafile = true)——普通条目不支持 side 与 flavors,这是最常见的坑(5.3)。
  2. 看 env:日志里 Detected env is … 是什么?use_envs 开了吗?(preset=minecraft 才有)
  3. 看口味:日志里有没有 Applying flavor choice X=Y from saved state(说明沿用了旧选择)→ 需要 -Dunsup.packwiz.changeFlavors=true 重新选。
  4. 看 unsup.toml 是否真被读到:pack.toml 的 [versions] 里有没有 unsup 键?文件是否与 pack.toml 同目录?
  5. 用 -Dunsup.dryRun=true 复跑一次,对着 6.4 的断言清单核对日志。

7.4 一致性错误与损坏诊断

  • 下载完成前,文件都在 .unsup-tmp/ 里;校验通过的才会在提交阶段落盘,所以中途失败不会留下半成品。
  • 覆盖本地文件前,如果该文件是冲突文件,会先改名成 <文件名>.orig。
  • hash 不匹配时 unsup 会尝试判断「是不是被某种工具改坏了」:Git LFS 指针文件、Unix→DOS / DOS→Unix 行尾转换,并针对 .js / .css / 图片给出「关掉压缩/优化」的提示(CorruptionHelper.java:43-82、UpdateHandler.java:487-503)。

7.5 代理与国内网络

  • 走代理:unsup.ini 里
    [http]
    proxy=http://127.0.0.1:7890     # 也支持 socks / socks5;写 none 表示不用代理,default 表示用系统默认
    代理 URI 不允许带用户名密码、不允许带路径(Config.java:296-342)。
  • 自定义 CA:[http] additional_cacert=<PEM 的 base64>;也可以整体关掉平台 / 内置 CA:http.use_platform_cacerts=false、http.use_builtin_cacerts=false(慎用)。
  • CHANGELOG 提到部分 CDN 在国内会被屏蔽,unsup 会对非 CDN 域名重试;实在不行就上代理。

第 8 章 附录

8.1 unsup.ini 全部键

必填:version(必须是 1)、source_format(packwiz / unsup / raw)、source(清单 URL;不能带用户名密码)。

键 默认 作用
preset — 继承内置预设,minecraft 最常用;内置 __global__ 永远先合并
use_envs false 启用环境机制
force_env — 强制环境,跳过探测
no_gui false 关掉界面
recognize_nogui false 启动参数含 nogui 时自动关界面
behavior manual auto(全自动)/ semi(自动更新、冲突询问)/ manual(都询问)
offer_change_flavors false 每次启动询问是否更改口味
subtitle 空 界面初始副标题
branding.modpack_name — 界面显示的整合包名
branding.icon — 界面图标
flavor_dialog_geom 600x400 口味对话框尺寸
flavor_dialog_bias 0.5 对话框位置偏向
public_key / alt_public_key — 签名公钥(signify / ed25519 / sigsum)
server_authority false 每次启动先检查 unsup.ini 自身是否需要更新
update_mmc_pack false(minecraft preset 为 true) 同步 MultiMC/Prism 组件
autofix true 自动修复可修复的不一致
enforce_secure_hashes false 是否强制安全哈希
puppet_mode auto auto / swing / opengl

分段:

段 内容
[colors] background title subtitle progress progress_track dialog button button_text question info warning error(RRGGBB)
[strings] 自定义界面文案;strings.dialog.progress.title* 禁止覆盖
[flavors] 组id=选项id,口味默认值
[env.<名>] marker=(可多行;* 为兜底;不带 / 的按类名处理)
[mmc-component-map] 包里的名字=MMC 组件 uid(可一对多,多行)
[authorization] <URL前缀>=Basic … / Bearer … / AWS4 …
[http] proxy=、additional_cacert=、insecure_host=、use_platform_cacerts=、use_builtin_cacerts=

依据:Config.java:124-353;内置预设 src/main/resources/diy/y2k/unsup/presets/{__global__,minecraft}.ini。

8.2 全部 -Dunsup.* 参数

参数 作用
-Dunsup.debug=true 详细日志、部分调试行为
-Dunsup.debug.requests=true 打印每个请求
-Dunsup.debug.pauseBeforeUpdate=true 应用更新前等 4 秒
-Dunsup.debug.overrideRemoteVersionCode=<n> 仅 native:强行指定远端版本号
-Dunsup.dryRun=true 只算计划、不写盘
-Dunsup.behavior=auto|semi|manual 覆盖 behavior
-Dunsup.downloadWorkers=<n> 并发下载数(默认 6)
-Dunsup.guiInStandalone=true standalone 也显示界面
-Dunsup.language=<BCP47> 强制界面语言;仓库内置 zh-CN、de、es、fr-FR、ko、nb-NO、nl、pl、pt、ru、sv-SE,另有玩具语言 tok、en-XA、en-PIG
-Dunsup.ignoreEnvs=true 忽略环境,全部文件都同步
-Dunsup.flavor.<组>=<选项> 覆盖口味默认值(不会覆盖状态文件里已保存的选择)
-Dunsup.bootstrapUrl=<URL> 本地无 unsup.ini 时从该地址拉一份
-Dunsup.bootstrapKey=<kind> <key> 校验 ${bootstrapUrl}.sig;首个空格可写成 %
-Dunsup.bootstrapAuth=<授权串> 拉取引导配置时带认证;同样可用 %
-Dunsup.abortOnPuppetCrash=true 界面崩溃时退出(调试界面用)
-Dunsup.puppetMode=auto|swing|opengl 界面实现
-Dunsup.puppetAsync=true 异步启动界面
-Dunsup.puppet.wrapperCommand=<cmd> 用命令包裹界面进程(调试用)
-Dunsup.puppet.passAllLwjglArgs=true 把全部 -Dorg.lwjgl.* 传给界面进程
-Dunsup.packwiz.changeFlavors=true 强制重新弹出 packwiz 口味选择

依据:SysPropDefs.java。

8.3 退出码

见 4.5。依据 ExitCode.java。

8.4 特殊 source:raw 与 merge

# 只同步单个文件,用于调试;本地文件名取 URL 路径最后一段
source_format=raw
source=https://example.com/files/test.jar#sha256:abcdef…   # 片段可省略,省略则每次都会重下
# 实验性:合并多个清单(用分号分隔)
source=merge:https://a.example/pack.toml;https://b.example/pack.toml

raw 的版本号固定是 debug 0 → debug 1,所以每次运行都会重下(RawHandler.java:39-48)。merge 会打日志 Using an experimental feature: Manifest merging,并分别维护各子源的状态(UpdateHandler.java:81-97)。

8.5 内置预设

preset=minecraft 会带来:

use_envs=true
recognize_nogui=true
update_mmc_pack=true

[env.client]
marker=net/minecraft/client/main/Main.class
marker=net/minecraft/client/Minecraft.class

[env.server]
marker=*

[mmc-component-map]
minecraft=net.minecraft
minecraft=net.fabricmc.intermediary
minecraft=org.quiltmc.hashed
unsup=com.unascribed.sup
unsup=com.unascribed.unsup
unsup=diy.y2k.unsup
fabric=net.fabricmc.fabric-loader
quilt=org.quiltmc.quilt-loader
liteloader=com.mumfrey.liteloader
forge=net.minecraftforge
neoforge=net.neoforged
nilloader=com.unascribed.nilloader

__global__ 预设只提供默认配色(深色 + 薄荷绿)。

8.6 来源与未验证项

来源:本仓库源码(关键处已在正文标注 文件:行号)、README.md、CHANGELOG.md、build.gradle、keys/README.md。

未验证:

  1. 未在真实 Minecraft 客户端 / 服务端 / Prism / MultiMC 实例上运行过;
  2. 未执行 ./gradlew build(构建产物路径、Prism 组件生成的表述来自 build.gradle 静态阅读);
  3. 本机 DNS 解析不了 git.sleeping.town,官方 wiki 原文未读,涉及 wiki 的内容标为〈搜索摘要〉;
  4. packwiz 侧行为(packwiz modrinth add --side、.packwizignore 与 refresh 的关系)来自〈搜索摘要〉;
  5. 运行时所需的 JDK 版本未实测;仓库的构建环境是 JDK 17(.java-version)。

官方资料:

  • 仓库与发布页:https://git.sleeping.town/exa/unsup
  • wiki:Config format、Manifest format(https://git.sleeping.town/exa/unsup/wiki/)
  • 界面翻译协作:Weblate(见 README.md 顶部徽章)
  • 许可证:LGPLv3(1.3.0 起包含 GPLv2 组件)
⚠️ **GitHub.com Fallback** ⚠️