unsup 中文教程 - SJMC-Dev/SMP3-modpack GitHub Wiki
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尊重)来自**〈搜索摘要〉**,未在本机验证。
- 你是玩家/服主?只需要两个文件:
unsup.jar+unsup.ini,放在工作目录里。 -
unsup.ini只回答一个问题:从哪个地址同步哪个包 —— 也就是source_format=和source=这两行,其余都是可选项。 -
pack.toml、index.toml、.pw.toml、unsup.toml、manifest.json、flavor、env这些东西都在「包」里,是包作者的地盘,本地一般不用碰。
远端 = 包的仓库(包作者维护)
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 只会把卡片指向的真实文件下载到工作目录。所以你在本地找不到它们,也就不用管它们。
| 命令 | 谁调用 | 跑完之后 | 界面 |
|---|---|---|---|
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。
| 谁决定 | 作用 | 一句话 | |
|---|---|---|---|
| env(环境) | 机器自动判定(agent 模式)或命令行指定(standalone) | 排除「这台机器根本不该有的文件」,例如服务端不要光影 | 「我这台是客户端还是服务端」 |
| flavor(口味) |
用户在对话框里选,结果记进 .unsup-state.json
|
在互不兼容的组合里选一套,例如「要不要装性能 mod」 | 「我要哪一套搭配」 |
重要的两条:
- 这两个规则都写在包里(由作者标在文件上),不是写在你的
unsup.ini里。 - 规则逐个文件生效,unsup 没有「整个目录排除」的写法。
所以「让服务端不要下 shaderpacks/」这类需求,必须由包作者在包里标(见 5.4、5.6);服主在本地配不出来。
versions.unsup 该填什么、metafiles_zip、puppet_mode、alt_public_key、server_authority、enforce_secure_hashes 这类,除非你开始发自己的包或排错,都用不到。
本章解决:把名词对上号。只想用起来可以跳过,遇到不懂的词再回来查。
-
读工作目录的
unsup.ini;读不到就什么都不做(Agent.java:236-307,日志No config file found? Doing nothing.)。 -
按
source_format选择处理方式,去source拉清单(UpdateHandler.java:107-113):source_format拉的东西 处理类 packwizpack.tomlPackwizHandlerunsup原生 manifest.jsonNativeHandlerraw单个文件(调试用) RawHandler -
比对本地的
.unsup-state.json与远端版本,生成更新计划(要下哪些、要删哪些)。 -
需要用户点头时(看
behavior、有没有版本更新、有没有文件冲突)弹对话框。 -
下载到
.unsup-tmp/,逐个校验 hash;校验通过才在提交阶段落盘。 -
写回
.unsup-state.json,输出Update successful!(UpdateHandler.java:454)。 -
agent 模式把控制权交还游戏;standalone 模式直接结束。
| 名词 | 一句话解释 |
|---|---|
| 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 给清单或引导配置签名,防止被篡改 |
| 文件 | 格式 | 在哪 | 谁维护 |
|---|---|---|---|
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 自动写 |
本章解决:怎么把包装上、怎么更新、弹窗怎么处理。服主请跳到第 4 章。
-
下载
unsup-<版本>.jar(发布页同时提供.sig签名文件和 Prism 用的diy.y2k.unsup.json)。 -
把
unsup.jar和整合包作者提供的unsup.ini都放进游戏的工作目录(原版启动器就是.minecraft)。 -
在启动器的 JVM 参数里加:
-javaagent:unsup.jar -
正常点「开始游戏」。unsup 会先同步、再让游戏启动;已经是最新版就什么都不显示。
也可以强制指定环境(等号后面跟环境名):
-javaagent:unsup.jar=server
路径相对工作目录解析,写绝对路径也行。agent 参数必须排在
-jar之前。
- 从最新 release 下载
diy.y2k.unsup.json,在 Prism Launcher 里作为 component 导入。 - 之后 Prism 会下载并管理 unsup,unsup 自己也能被更新。
- 包里如果声明了组件版本(
pack.toml的[versions]+unsup.ini的[mmc-component-map]),还能顺带更新 mod loader / Minecraft 本体。组件被更新后 unsup 会退出并提示重启(见 4.6)。 - MultiMC 不支持 java agent 形式的组件。
-
首次同步(本地没有
.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 会让游戏照常启动。
默认策略是「只在该问你的时候才出现」:
- 有更新要确认(
behavior=manual时)→ 弹更新对话框; - 有文件冲突 → 弹冲突对话框;
- 有没选过的 flavor 组 → 弹口味选择对话框;
- 一切正常且无事可问 → 不显示窗口(源码为此设了 1250ms 延迟再显示,避免闪一下)。
冲突的含义是:你本地改过的文件,正好这次也要被上游更新(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时不询问,全部按「是」处理。
| 现象 | 原因 / 处理 |
|---|---|
| 点了开始,游戏正常起,但没更新 | 工作目录没有 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(不动盘,只打印计划) |
本章解决:服务器怎么同步、怎么不弹窗、退出码什么意思。玩家请回第 3 章。
# 在服务器根目录(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不参与判断(默认就已经没界面了)。
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(内置minecraftpreset 默认开启)在纯服务端上没有副作用:找不到../mmc-pack.json时只输出一行update_mmc_pack is enabled, but I don't see an mmc-pack.json. Ignoring(MMCUpdater.java:48-52)。
优先级从高到低(Config.java:238-294):
-
命令行给的:
java -jar unsup.jar server或-javaagent:unsup.jar=server。 -
unsup.ini的force_env=。 -
自动探测(仅 agent 模式):遍历
[env.<名>]的marker=,用 classloader 找这些资源——-
marker=*表示「以上都不匹配就归我」; - 不带
/的 marker 会被当成类名(.→/并补.class),例如net.minecraft.client.main.Main。
-
use_envs=true 时,如果判定不出环境,或给了不在合法名单里的环境名,unsup 会列出有效值并以退出码 1 退出。
- 内置
minecraftpreset 的[env.server] marker=*就是服务端的兜底:服务端 classpath 里没有客户端类,于是自动算server。 - 想忽略环境、所有文件都下:
-Dunsup.ignoreEnvs=true(SysProps.IGNORE_ENVS)。
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)。
| 退出码 | 枚举 | 含义 |
|---|---|---|
| 0 | SUCCESS |
成功 |
| 1 | CONFIG_ERROR |
配置错误(缺必填项、格式错误、env 判定失败等) |
| 2 | CONSISTENCY_ERROR |
一致性 / 状态文件错误(.unsup-state.json 读不了) |
| 3 | BUG |
内部错误 |
| 4 | USER_REQUEST |
用户取消(关窗口、点取消) |
| 5 | BOOTSTRAP_FAILED |
引导配置(unsup.bootstrapUrl)下载失败 |
来源:ExitCode.java。
如果启用了组件同步(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; - 必须重启游戏才会真正生效。
| 现象 | 原因 / 处理 |
|---|---|
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 更详细 |
本章解决:包怎么写、文件怎么标
side与flavor、unsup.toml怎么用、签名怎么弄。纯玩家/服主可以跳过。
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)。
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),以此决定要不要更新。
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 = falsehash-format 支持 md5 / sha1 / sha256 / sha384 / sha512 / murmur2(murmur2 是 CurseForge 的,unsup 会先转成十六进制;PackwizHandler.java:640-650)。
在 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;
}生效前提:
-
use_envs=true(preset=minecraft已默认开启); - 那台机器确实被判成目标环境(服务端通常是
server,见 4.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)。
位置与启用条件:
- 必须放在
pack.toml同目录,文件名固定unsup.toml; - 只有
pack.toml的[versions]里有unsup这个键时才会被读取(PackwizHandler.java:203-208)。
支持三块内容:
[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.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 = true开启后 unsup 会下载 unsup-metafiles.zip.br(brotli 压缩的 zip,里面是所有 .pw.toml),把「N 个 metafile = N 次请求」压成 1 次;每个条目仍会重新校验 hash,条目缺失或 hash 不符只警告并回退逐个下载(PackwizHandler.java:279-299、360-386)。它是可选的分发物,由作者/CI 生成,没有它 unsup 照常工作,只是慢一点。
前提:flavor 的粒度是单个文件,标签内容是选项 id,没有目录/通配符写法;packwiz 模式下只有 metafile 能被标。
方式 1:packwiz 原生 option.optional(最省事)
[option]
optional = true
default = falseunsup 会自动合成一个 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 的增量链)。
[versions]
unsup = "1.3"-
开关:判定条件是
containsPrimitive("unsup")—— 只要键在,unsup 就会去读unsup.toml,值写什么都一样;读不到只打一条 debug 日志unsup is in the versions table, but there's no unsup.toml。 -
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会导致每次都判定「有组件更新」→ 更新后退出重启。
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)。
- packwiz 模式:
-
不用签名也不算错,但
source是明文http时 unsup 会警告Using unencrypted HTTP without manifest signing - this is a very bad idea!(本地回环地址不触发;UpdateHandler.java:100-106)。
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数组。
| 现象 | 原因 |
|---|---|
加了 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 就永远不会同步它——但反过来,客户端也拿不到它,且已经在客户端/服务端的那些副本会在下一次同步时被删掉。
本章解决:包改完了,怎么在自己机器上确认「玩家拿到的是对的」。
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 = truemods/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 用法见其官方文档)。
# 在 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排除了本机地址)。
# 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 秒)。
| 日志行 | 说明 |
|---|---|
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. |
组件更新了,需要重启 |
| 场景 | 怎么跑 | 期望 |
|---|---|---|
| 作者本机当客户端(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 不稳定。
rm -rf .unsup-state.json .unsup-tmp unsup.log # Windows: del / rmdir.unsup-state.json 里记着已同步状态与口味选择;想重新体验「首次同步」就删掉它。
本章解决:出问题了从哪看、怎么定位。所有排查都从
unsup.log开始。
- 工作目录下的
unsup.log:debug 级内容始终写入,即使没开-Dunsup.debug=true。 - 想更详细:
-Dunsup.debug=true、-Dunsup.debug.requests=true(打印每个请求)。 - 想看更新计划而先不动盘:
-Dunsup.dryRun=true。
| 症状 | 可能原因 | 处理 |
|---|---|---|
| 什么都不发生 | 工作目录没有 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 设置 |
- 看 index:这条是 metafile 吗?(
metafile = true)——普通条目不支持side与flavors,这是最常见的坑(5.3)。 - 看 env:日志里
Detected env is …是什么?use_envs开了吗?(preset=minecraft才有) - 看口味:日志里有没有
Applying flavor choice X=Y from saved state(说明沿用了旧选择)→ 需要-Dunsup.packwiz.changeFlavors=true重新选。 - 看
unsup.toml是否真被读到:pack.toml的[versions]里有没有unsup键?文件是否与pack.toml同目录? - 用
-Dunsup.dryRun=true复跑一次,对着 6.4 的断言清单核对日志。
- 下载完成前,文件都在
.unsup-tmp/里;校验通过的才会在提交阶段落盘,所以中途失败不会留下半成品。 - 覆盖本地文件前,如果该文件是冲突文件,会先改名成
<文件名>.orig。 - hash 不匹配时 unsup 会尝试判断「是不是被某种工具改坏了」:Git LFS 指针文件、Unix→DOS / DOS→Unix 行尾转换,并针对
.js/.css/ 图片给出「关掉压缩/优化」的提示(CorruptionHelper.java:43-82、UpdateHandler.java:487-503)。
- 走代理:
unsup.ini里代理 URI 不允许带用户名密码、不允许带路径([http] proxy=http://127.0.0.1:7890 # 也支持 socks / socks5;写 none 表示不用代理,default 表示用系统默认
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 域名重试;实在不行就上代理。
必填: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。
| 参数 | 作用 |
|---|---|
-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。
见 4.5。依据 ExitCode.java。
# 只同步单个文件,用于调试;本地文件名取 URL 路径最后一段
source_format=raw
source=https://example.com/files/test.jar#sha256:abcdef… # 片段可省略,省略则每次都会重下# 实验性:合并多个清单(用分号分隔)
source=merge:https://a.example/pack.toml;https://b.example/pack.tomlraw 的版本号固定是 debug 0 → debug 1,所以每次运行都会重下(RawHandler.java:39-48)。merge 会打日志 Using an experimental feature: Manifest merging,并分别维护各子源的状态(UpdateHandler.java:81-97)。
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__ 预设只提供默认配色(深色 + 薄荷绿)。
来源:本仓库源码(关键处已在正文标注 文件:行号)、README.md、CHANGELOG.md、build.gradle、keys/README.md。
未验证:
- 未在真实 Minecraft 客户端 / 服务端 / Prism / MultiMC 实例上运行过;
- 未执行
./gradlew build(构建产物路径、Prism 组件生成的表述来自build.gradle静态阅读); - 本机 DNS 解析不了
git.sleeping.town,官方 wiki 原文未读,涉及 wiki 的内容标为〈搜索摘要〉; - packwiz 侧行为(
packwiz modrinth add --side、.packwizignore与refresh的关系)来自〈搜索摘要〉; - 运行时所需的 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 组件)