数据驱动 JSON 格式规范(schema)¶
类型:Reference(信息向,供查阅)。 适用版本:Minecraft 1.21.1、NeoForge 21.1.203、Parchment 2024.11.17、Java 21。本页描述的格式只在该版本区间内成立。 代码基准:
KasugaLibNext分支model-loader,核验日期 2026-10-04,基准 commitec864a1。文中行号对应当前工作树(未提交的 data-driven 重实现)。 权威来源:本页规则来自当前代码与测试。旧sources布局已迁移到索引清单;每条规则给出类、行号或测试名,读者可自行核对。
0. 术语与范围¶
0.1 三个必须分清的概念¶
- 注册期(registration phase):mod 启动、构造期间把对象写入 MC 注册表(如
BuiltInRegistries.BLOCK)的阶段,读取来源是 mod jar,一次完成,不可热更。 - reload 期(reload phase):
/reload触发的资源重读,读取来源是包栈(pack stack),数据包可覆盖。 - 索引文件(index manifest):
data/<namespace>/kasuga_lib/data_driven/下的.json,只声明「哪些内容文件属于哪个域」,本身不含任何注册对象。
一个 mod 的索引文件可以只声明注册域、只声明 reload 域,或两者都声明。
0.2 本文覆盖什么¶
覆盖:索引文件格式、注册期内容文件格式(registry_groups / blocks / items / 内嵌 block_entity)、重复 id 规则、reload 期内容文件 wrapper 格式、错误的分类与出口。
不覆盖:各 properties 键在注册表里的具体效果(见 guide-content.md)、状态机定义与动画片段元素内部的完整 codec 字段(见 ../../reference/fsm.md 与 uml/ 下的 codec)、Java 侧 TypeHandler 扩展(见 guide-extension.md)。
1. 索引文件(index manifest)¶
1.1 位置与发现规则¶
索引目录固定为 4 段路径:
data/<namespace>/kasuga_lib/data_driven/
- 由
JsonTreeBuilder.indexDirectorySegments(modId)锁定,返回{"data", modId, "kasuga_lib", "data_driven"}(JsonTreeBuilder.java:745-747)。 - 注意
kasuga_lib是固定拼写,data_driven用下划线。写成kasuga-lib或漏掉kasuga_lib这一层都会导致静默加载 0 条;该风险由JsonTreeBuilderIndexPathTest.indexDirectorySegmentsMatchCanonicalLayout()用assertArrayEquals锁死(JsonTreeBuilderIndexPathTest.java:27-33)。 - 目录下所有
.json文件都被当作索引,按文件名升序读取(JsonTreeBuilder.listIndexFiles,:762-774;resolveIndexManifests按传入顺序聚合)。文件名无约定,index.json只是惯例。 - 目录不存在时只记 DEBUG,不算错误(
JsonTreeBuilder.java:77)。 - reload 侧读同一目录时复用同一组路径段(
ReloadOrchestrator.indexResourcePath,:290)。
1.2 顶层字段¶
索引文件的顶层字段只有两个,各自是字符串数组:
| 字段 | 类型 | 域 | 含义 |
|---|---|---|---|
on_register |
string[] |
注册期 | 列出在 mod 构造期解析、写入 MC 注册表的内容文件 |
on_reload |
string[] |
reload 期 | 列出在 /reload 解析、写入 reload 桶的内容文件 |
两者都可选,但至少出现一个(D1/D2)。字段常量在 JsonTreeBuilder.java:40,42;解析在 parseIndexManifest(:222-250)。
一个合法的双域索引:
{
"on_register": [
"kasuga_lib_content/blocks.json",
"kasuga_lib_content/groups.json"
],
"on_reload": [
"state_machines/panel.json",
"animation_clips/wheel.json"
]
}
1.3 sources 已删除(破坏性变更)¶
sources 现在是非法字段。 它不再被识别、不再有别名,走的是「未知顶层字段」通道:记 error + 入桶,并且该文件因此「什么都没声明」(再记一条「两者皆未声明」error)。
{
"sources": ["kasuga_lib_content/blocks.json"]
}
上例会产生两条 error:(1) contains unsupported field 'sources';(2) declares neither 'on_register' nor 'on_reload'。写法见 JsonTreeBuilder.parseIndexManifest(:226-230 未知字段、:240-244 两者皆未声明);由 JsonTreeBuilderIndexSchemaTest.legacySourcesFieldIsReportedAsUnknown() 锁定。
旧字段 sources 与旧平铺布局 data/<namespace>/kasugalib/ 都已硬迁移删除,不做兼容期。若磁盘上存在旧目录且没有新索引目录,会打一条 WARN + 入桶并提示迁移到 on_register(JsonTreeBuilder.java:65-79)。
1.4 字段级规则 D1–D6¶
这些规则的可执行版本是 JsonTreeBuilderIndexSchemaTest(8 个用例,见 §6)。规则以 parseIndexManifest(单文件)与 resolveIndexManifests(跨文件聚合)实现。
- D1 至少声明一个字段:两个字段都不出现(空文件、只有未知字段的文件)→ error + 入桶,该文件不贡献任何路径。空文件(GSON 解析为
null)同样报错(JsonTreeBuilder.java:240-244;测试declaringNeitherDomainIsReportedAndConsumesNothing/emptyFileIsReportedInsteadOfSilentlyIgnored)。 - D2 空数组合法:
{"on_register": []}是合法的,且计为已声明(JsonTreeBuilder.java:237;测试emptyArrayIsValidAndCountsAsDeclared)。 - D3 单字段失败不连累兄弟字段:
on_register非数组时只报该字段错,on_reload照读,反之亦然(readPathArray,:256-277;测试nonArrayFieldDoesNotAbortTheSibling)。数组里非字符串的元素报错并跳过,其余元素保留(:268-274)。 - D4 聚合告警按「是否声明过」分支:只声明
on_reload的纯 reload mod 不报「未声明注册域」;只有任何文件都没声明任一字段时,聚合阶段才记一条 WARN + 入桶(resolveIndexManifests,:323-326;测试pureReloadModDoesNotRaiseTheAggregateError)。单文件层面都声明失败时聚合错误照报(测试allBrokenSchemaStillRaisesTheAggregateError)。 - D5
sources是未知字段,不是别名(:226-230;测试legacySourcesFieldIsReportedAsUnknown)。 - D6 同一路径双列 fail-closed:同一路径(同文件内或跨文件)同时出现在两个数组 → error + 入桶,且该路径两个域都不消费(从两个列表里都删除)。判定跨该 mod 的全部索引文件(
resolveIndexManifests,:308-316;测试crossListedPathIsConsumedByNeitherDomain)。
1.5 路径条目规则¶
数组元素是相对于 data/<namespace>/ 的路径,必须带 .json 后缀。校验在 JsonTreeBuilder.validateSourcePath(:383-392),reload 侧复用同一函数(ReloadOrchestrator.collectIndex,:369)。违反任一条 → error + 入桶并跳过该条:
| 规则 | 违反时的消息片段 |
|---|---|
| 非空 | path is empty |
不以 / 开头(必须相对) |
path must be relative |
以 .json 结尾 |
path must include the '.json' suffix |
无空段(//) |
path contains an empty segment |
不含 . 或 .. 段 |
path must not contain '.' or '..' |
补充:
- 路径不可跨命名空间是结构性的:
parseSource永远把路径拼在data/<modId>/之下(JsonTreeBuilder.java:414-420)。也正因如此,指向assets/的条目不可能生效。 - 同一路径在同一 mod 内被多处列出 → 静默去重,只解析一次(
parseSource的parsedSources,:404),不报错。 - reload 侧还要求路径能构成合法
ResourceLocation(只允许[a-z0-9/._-]);大写等非法字符会单独报错(ReloadOrchestrator.java:369)。 - 路径非法、文件缺失、JSON 不可读都会 error + 入桶(
parseSource,:395-436)。
1.6 多清单合并¶
同一 mod 的全部索引文件先全部读入,再统一消费:路径按「索引文件名升序 → 文件内数组书写顺序」拼接(JsonTreeBuilder.java:92-96,296-301)。因此 D6 的双列检测能跨文件生效,每条内容文件在一轮加载内至多解析一次。
2. 注册期内容文件(列在 on_register)¶
2.1 文件顶层结构¶
内容文件的顶层字段名 = 顶层注册类型,值必须是对象数组。当前有 3 个顶层类型:
| 顶层字段 | handler | phase | 作用 |
|---|---|---|---|
registry_groups |
RegistryGroupHandler |
0 | 建立属性分组(父/子) |
blocks |
BlockTypeHandler |
1 | 注册方块 |
items |
ItemTypeHandler |
1 | 注册独立物品 |
block_entities 不是顶层字段,它是内嵌类型,写法见 §2.5。
一个内容文件可同时包含多个顶层字段(例如 registry_groups 与 blocks 各一组),由 dispatchContent 逐个分派(JsonTreeBuilder.java:461)。
{
"registry_groups": [
{ "id": "mymod:panels", "properties": { "no_occlusion": true } }
],
"blocks": [
{
"id": "mymod:panel",
"type": "simple_block",
"registry_group": "mymod:panels",
"properties": { "destroy_time": 2.0 },
"item_properties": { "tab": "mymod:main" }
}
]
}
顶层字段的校验:
- 未知顶层字段 → error + 入桶,且同文件其余字段照常分派(半应用;
JsonTreeBuilder.java:475-490)。 - 已知字段但值不是数组 → error + 入桶,其余字段照常(
:504-510)。 - 数组元素不是 JSON 对象 → error + 入桶,该元素跳过,其余元素照常(
:504-516)。
2.2 registry_groups 条目¶
解析见 RegistryGroupHandler.parse(RegistryGroupHandler.java:26-33),应用见 store(:54-90)。
| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
id |
string | 是 | — | 分组 id。按原字符串匹配,不做命名空间归一化 |
parent |
string | 否 | null → 挂根组 |
父分组 id;父不存在时回退挂根组(:58-63) |
properties |
object | 否 | — | 传给子内容的方块属性(见 §2.6) |
item_properties |
object | 否 | — | 传给子内容的物品属性;其中 tab 单独抽出作为该组的创造栏(:73-76) |
- 分组没有
type字段。 - 方块/物品通过
registry_group用原字符串查组;因此id不归一化,mymod:panels与panels是两个不同的组(RegistryGroupHandler.resolveIdentity,:49-52)。 - 应用前按
parent做拓扑排序:父先于子;环只打 WARN,不报错,回退原顺序(JsonTreeBuilder.topoSortGroupDefs,:869)。
2.3 blocks 条目¶
解析见 BlockTypeHandler.parse(BlockTypeHandler.java:25-34),通用应用见 RegTypeHandler.apply(RegTypeHandler.java:62-112)。
| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
id |
string | 是 | — | 方块 id,namespace:path 或裸路径 |
type |
string | 是 | — | 工厂名,查 FactoryRegistry.BlockFactory(BlockTypeHandler.createRegistration,:59-64) |
registry_group |
string | 否 | null → 挂根组 |
所属分组 id(原串匹配) |
properties |
object | 否 | — | 方块属性,见 §2.6 |
item_properties |
object | 否 | — | 方块对应的物品属性;tab 决定创造栏(BlockTypeHandler.resolveCreativeTab,:51-57) |
params |
object | 否 | null |
原样传给工厂 create(path, params) |
state_machine |
string | 否 | — | 仅当条目内含 block_entity 时被转发进 BE params(见 §2.5) |
id与type缺失时parse抛异常(json.get("id").getAsString()),该异常被注册期元素级容错捕获 → error + 入桶,只丢该条目、同文件兄弟条目照常注册(JsonTreeBuilder产出entry <i> in '<key>' failed to parse: <msg>; entry skipped)。其余字段缺失走默认值。type找不到对应工厂 → 只打 WARN,条目跳过,不入桶(RegTypeHandler.java:70-74)。- 对象的未知键被静默忽略(parse 只读已知键),这一点与文件顶层未知键报错相反,见 §2.7。
2.4 items 条目¶
解析见 ItemTypeHandler.parse(ItemTypeHandler.java:22-30)。
| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
id |
string | 是 | — | 物品 id |
type |
string | 是 | — | 工厂名,查 FactoryRegistry.ItemFactory(:56-60) |
registry_group |
string | 否 | 挂根组 | 所属分组 id |
properties |
object | 否 | — | 物品属性,见 §2.6;tab 决定创造栏(:48-53) |
params |
object | 否 | null |
传给工厂 |
2.5 block_entities(内嵌类型)¶
方块实体没有自己的 id,它由宿主方块承载,写法是把一个 block_entity 对象放进该方块条目里:
{
"blocks": [
{
"id": "mymod:be_block",
"type": "be_block",
"block_entity": {
"type": "test_be",
"params": { "tick_interval": 4 }
}
}
]
}
- 顶层字段名是
block_entities,但内嵌键名是block_entity(BlockEntityTypeHandler.getTypeName:21、getParentTypeName:27、getEmbeddedKeyName:30)。 - 提取逻辑:
BlockEntityTypeHandler.extractEmbedded(:33-44)把宿主方块的id注入内嵌对象的_parent_block;若宿主方块顶层有 JSON 原始类型(primitive:string / number / boolean)的state_machine,再转发进内嵌对象的params.state_machine(:37-42)。判据是isJsonPrimitive(),故 number / boolean 也放行,经getAsString()转成文本后存入params;对象、数组、null 被忽略。
内嵌 block_entity 对象的字段:
| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
type |
string | 是 | — | 方块实体工厂名,查 FactoryRegistry.BlockEntityFactory |
params |
object | 否 | null |
传给工厂;常见键 model / state_machine 由具体工厂解释 |
_parent_block |
string | — | 注入 | 不要手写,由 extractEmbedded 注入 |
- 重复判定:两条 BE 条目挂在同一个宿主方块上即视为重复(
resolveIdentity,:66-68,用宿主方块的 effective id)。 - 宿主方块找不到 / 未知 BE 工厂 → 只打 WARN,不入桶(
BlockEntityTypeHandler.apply,:75-86)。
2.6 properties / item_properties 内部键¶
顶层字段的 properties 与 item_properties 是开放对象,键由两个解析器认领;未知键只打 WARN 并忽略,不报错。
方块属性 properties(JsonPropertyParser.buildCompilers,JsonPropertyParser.java:148-174):
| 键 | 值类型 | 键 | 值类型 |
|---|---|---|---|
no_occlusion |
bool | light_emission |
int |
no_collision / no_collission(拼写别名) |
bool | friction |
float |
requires_correct_tool |
bool | speed_factor |
float |
replaceable |
bool | jump_factor |
float |
dynamic_shape |
bool | destroy_time |
float |
random_ticks |
bool | explosion_resistance |
float |
no_loot_table |
bool | strength |
float 或 [destroy, resist] 两元数组 |
ignited_by_lava |
bool | map_color |
DyeColor 名(小写) |
liquid |
bool | sound_type |
声音类型名(见下) |
force_solid_on |
bool | ||
air |
bool | ||
no_terrain_particles |
bool |
sound_type的合法值来自JsonPropertyParser.buildSoundTypeMap(:97-146),如stone/wood/metal/glass等;未知值只 warn。map_color用 MCDyeColor名,未知值只 warn。
物品属性 item_properties(方块条目)/ properties(物品条目)(JsonItemParser,JsonItemParser.java:20-36):
| 键 | 值类型 | 含义 |
|---|---|---|
stacks_to |
int | 最大堆叠数 |
rarity |
string | COMMON / UNCOMMON / RARE / EPIC(大小写不敏感) |
fire_resistant |
bool | 防火 |
durability |
int | 耐久 |
no_repair |
bool | 不可修复 |
tab |
string | 创造栏 ResourceLocation;由 loader 单独消费,不进入上述修饰器 |
属性沿父链继承,子条目覆盖父条目(RegTypeHandler.apply 设 registry_group 为父,Reg.applyProperties 父先子后)。
2.7 对象内未知键 vs 文件顶层未知键¶
两种位置的严格度不同,是最容易踩的差异:
- 文件顶层未知字段 → error + 入桶(
dispatchContent,JsonTreeBuilder.java:475-490)。拼错blokcs会被抓住。 - 对象内未知键 → 静默忽略(各 handler 的
parse只读已知键),不报错。例如 block 条目里多写一个foo不产生任何诊断。
2.8 内嵌类型误写为顶层字段¶
把 block_entities 当顶层字段写(而不是放进 blocks 条目的 block_entity 键)会走「未知顶层字段」通道:error + 入桶,消息里点名「应写在 blocks 条目的 block_entity 键」,并且同文件其余的 blocks 照常分派。
{
"block_entities": [ { "type": "test_be" } ],
"blocks": [ { "id": "mymod:ok", "type": "simple_block" } ]
}
行为由 JsonTreeBuilderEmbeddedTypeDispatchTest.topLevelEmbeddedTypeIsReportedAndTheRestOfTheFileStillDispatches() 锁定。这只说明「误写为顶层字段」的情形;内嵌对象自身若因缺字段解析失败,异常同样被元素级容错捕获,只丢该内嵌元素、同文件其余条目照常(同 §2.3 的 id/type 缺失)。
3. 重复 id 与 last-wins¶
去重在任何注册副作用之前完成:解析全部结束后先裁决,败者根本不进入 apply(JsonTreeBuilder.resolveDuplicates,:652)。这是因为 MC 的 MappedRegistry.register 不拒绝重复 key,重复 apply 会留下不一致条目(DuplicateIdResolver.java:11-14)。
3.1 判定 identity¶
冲突空间是 (typeName, identity),不同类型同 id 不算冲突。各类型的 identity(TypeHandler.resolveIdentity):
| 类型 | identity | 归一化 |
|---|---|---|
blocks / items |
EffectiveId.of(modId, id) |
无命名空间或 minecraft: 前缀 → <modId>:path;其它命名空间原样保留 |
block_entities |
宿主方块的 effective id | 同上 |
registry_groups |
id 原字符串 |
不归一化 |
EffectiveId.of 见 EffectiveId.java:39-52,表测试 EffectiveIdTest(unnamespacedIdsResolveUnderTheModNamespace / explicitMinecraftNamespaceResolvesToTheOwningMod / otherNamespacesAreKeptVerbatim)。
3.2 「后」的确切含义¶
last-wins 的「后」是加载序列中的位置,不是路径的字典序:
- 索引文件之间:文件名升序;
- 一个索引文件内:
on_register数组书写顺序; - 一个内容文件内:类型字段数组书写顺序;
- 内嵌 BE:跟随宿主
blocks条目的位置。
依据:JsonTreeBuilder 按上述顺序构造候选序列(:105-110,655),DuplicateIdResolver.resolve 只按传入下标取最后一个(:73-100)。DuplicateIdResolverTest.lastMeansLastInSequenceNotLexicographicallyLastSource() 专门锁定「路径字典序靠前但加载序靠后时仍胜出」。
3.3 结果¶
- 胜者进入
apply;败者被整条丢弃,其内嵌 BE 一并隔离(JsonTreeBuilder.java:678)。 - 每个冲突记 WARN 日志 + 一条 loading error 桶条目(
:667-670)。注意:日志级别是 WARN,但落在错误桶里。 - 有冲突时另记一条汇总 WARN(
:672-674)。 - 诊断消息含类型字段、id、胜者与败者的来源路径(
DuplicateIdResolver.describe,:109-115)。
4. reload 期内容文件(列在 on_reload)¶
4.1 两类内容与发现方式¶
ReloadOrchestrator 是 reload 域唯一的编排者(ReloadOrchestrator.java:121),它本身不认识任何类型名:每个已注册的 ReloadHandler 的 typeName() 就是一个合法顶层键,编排器遍历 ReloadHandlerRegistry 得到键集合。当前 modelling 侧注册的两个 handler 是 state_machines 与 animation_clips(FsmReloadHandler / FsmClipsReloadHandler,见 §4.6)。
| 内容 | 顶层键 | 发现方式 |
|---|---|---|
| 状态机定义 | state_machines |
目录 glob data/<ns>/state_machines/*.json + on_reload 列出 |
| 动画片段 | animation_clips |
仅 on_reload 列出 |
state_machines/有目录 glob:FsmReloadHandler.globDirectories()(FsmReloadHandler.java:69-71)声明StateMachineDefinitionLoader.PATH(StateMachineDefinitionLoader.java:39),由ReloadOrchestrator.collectGlob(:348-367)用ResourceManager.listResources列举。一个 state machine 文件即使没被任何索引列出也会被读。animation_clips/没有目录 glob。未被索引列出的 clip 文件永远不被读(AnimationClipLoader.java:28-29;FsmClipsReloadHandler.globDirectories保持默认空集)。- 索引列出的
state_machines/文件会被 glob 入口跳过,避免读两次(ReloadOrchestrator.collectGlob,:348-367)。 - 应用顺序:glob 发现在前、索引列出在后;同 id 后者胜(类文档
:77-91;register,:507-526)。
4.2 state_machines wrapper¶
每个文件是一个携带 state_machines 顶层键的包装对象,值必须是定义数组(多余的顶层键不再拒绝文件,见 §4.4):
{
"state_machines": [
{
"id": "mymod:panel",
"layers": [
{
"id": "base",
"initial_state": "idle",
"states": [
{ "id": "idle", "duration_ticks": 20 }
]
}
]
}
]
}
- 一个文件可放多个定义,数组顺序即文件内 last-wins 顺序(
StateMachineDefinitionLoader.java:14-33)。 - 元素 schema 由
StateMachineDefinition.CODEC决定;本页只规范 wrapper,元素字段见../../reference/fsm.md与对应 codec。
4.3 animation_clips wrapper¶
同样是携带 animation_clips 顶层键的包装对象:
{
"animation_clips": [
{
"id": "mymod:wheel_spin",
"duration_seconds": 2.5,
"bones": [
{
"bone": "wheel_r",
"keyframes": [
{ "time": 0.0, "transform": { "rotate": [0, 0, 0] } }
]
}
]
}
]
}
- 元素自带
id(就是states[].clip引用的 id),loader 不额外抽取 id(AnimationClipLoader.java:21-27)。 - 元素里 codec 未命名的键被忽略,可作注释/编辑器元数据(
:26-27)。 - 文件只在
on_reload中生效,注册进 reload 桶;同名 SCRIPT 片段不被文件覆盖(AnimationClipLoader.register,:113-121)。
4.4 文件级严格、元素级容错¶
两个 decoder 的失败隔离一致(StateMachineDefinitionLoader.decodeFile :67-100;AnimationClipLoader.decodeFile :69-102):
- 文件级:非对象 body、缺顶层键、值非数组 → 整个文件拒绝,报一条 error + 入桶。
{"state_machines": {}}会被整体拒绝(StateMachineDefinitionLoaderTest.nonArrayValueRejectsWholeFile)。 - 文件级(真未知键):顶层出现不被任何 reload 类型认领的键 → 报一条
unsupported top-level field 'extra'+ 入桶,但已知键照常贡献(半应用,不再整文件拒绝)。{"state_machines": [...], "extra": 1}中state_machines照常注册(StateMachineDefinitionLoaderTest.extraTopLevelKeyDoesNotAffectThisType();AnimationClipLoaderTest.ignoresAnExtraTopLevelKey())。 - 多顶层键合法:同一文件可同时携带
state_machines与animation_clips,两个 decoder 各读自己的键、各贡献各的条目(AnimationClipReloadTest.mixedTopLevelKeysAreConsumedByBothHandlers)。 - 元素级:数组元素不是对象、或缺字段解不出 → 只跳过该元素,兄弟照常,逐个报错。见
StateMachineDefinitionLoaderTest.badElementIsSkippedWhileSiblingsDecode()与AnimationClipLoaderTest.skipsAMalformedElementAndKeepsItsSiblings()/skipsAnElementWithoutAnId()。 - 空数组合法(
AnimationClipLoaderTest.emptyClipArrayIsValid())。
4.5 reload 的域路由与反向 D9 提示¶
- 域由索引数组决定,内容类型由顶层键决定。
- reload 文件里的未知顶层键 → error + 入桶,并附反向提示:「若这些字段是注册内容(如
blocks/items/registry_groups),请把文件改列到on_register」(ReloadOrchestrator.java:439-446)。 - 一个文件可以同时承载两类内容:两个 handler 各认领自己的顶层键、各贡献各的条目(
ReloadOrchestrator.dispatch,:429-470)。未知顶层键由ContentStructure.unknownKeys判定,不再有「唯一顶层键」检查。 - reload 收尾会复核状态机引用的 clip;悬空引用只记 WARN + 入桶,不影响注册(
FsmReloadHandler.afterReload,:95-116)。
4.6 扩展到新 reload 类型¶
reload 类型不是硬编码的:继承/实现 ReloadHandler<T>,用一个 @Context bean 的 @PostConstruct 调 ReloadHandlerRegistry.register(handler) 注册即可,不需要改编排器。typeName() 就是该类型认领的顶层键,也兼作去重碰撞空间。详细步骤与最小示例见 guide-extension.md §6,接口签名见 api-reload.md §13。
5. 错误的分类与出口¶
5.1 错误(error 日志 + 入桶)¶
注册期与 reload 期的格式错误基本都在此列:
| 情形 | 出处 |
|---|---|
| 索引未知字段 / 字段非数组 / 元素非字符串 | JsonTreeBuilder.java:226-274 |
| 单文件未声明任一域;全部文件都未声明 | :240-244(error)/ :323-326(WARN + 桶) |
| 同一路径双列 | :308-316 |
| 索引文件或目录读取失败 | :188、:767-771 |
| 内容文件顶层未知字段、字段非数组、元素非对象 | :475-516 |
| 内容文件路径非法 / 文件缺失 / 解析异常 | :396-436 |
handler apply 抛异常 |
:712 |
| 重复 id 冲突 | :667-670(WARN 日志 + 桶条目) |
| reload 侧:清单读取失败、路径非法、文件缺失、wrapper 形状违反、未知顶层键 | ReloadOrchestrator.java:307,369,405-470 |
| reload 重复 id | :545-556(WARN 日志 + 桶条目) |
| reload 收尾悬空 clip 引用 | FsmReloadHandler.java:111-116(WARN 日志 + 桶条目) |
5.2 警告(warn,不入桶)¶
以下情形只打日志、不写桶,因为条目被降级而非格式错误:
- 未知工厂
type→ 条目跳过(RegTypeHandler.java:70-74)。 - 未知
properties/item_properties键 → 该键忽略(JsonPropertyParser.java:50-52;JsonItemParser.java:63)。 - 未知
map_color/sound_type(JsonPropertyParser.java:80-82,90-92)。 - BE 宿主方块找不到 / 未知 BE 工厂(
BlockEntityTypeHandler.java:77,83)。 - 分组引用环(
JsonTreeBuilder.java:823-825)。
5.3 诊断读取¶
所有错误按 mod id 分桶,读取 API:
JsonTreeBuilder.getLoadingErrors(modId)/getLoadingErrorsByMod()/getLoadingErrors()(JsonTreeBuilder.java:743-775)。- 统一门面
Diagnostics:Diagnostics.errors(modId)、Diagnostics.errors(Domain, key)、Diagnostics.summarize()(Diagnostics.java:64-166)。mod 维逐字委托JsonTreeBuilder的桶(注册期);域 × source 维已有写入方——reload 期用Domain.RELOAD_DATA、键 = 命名空间(Diagnostics.java:21-31,96-105)。 - 测试断言契约:
assertEquals(List.of(), JsonTreeBuilder.getLoadingErrors(modId))。
5.4 D9 提示文案(跨域误列)¶
两个方向的条件提示,错误消息字符串是公共契约:
- 注册侧:
on_register列了 reload 域文件(顶层是state_machines之类)→ 走未知字段通道,消息尾部附「list this file under'on_reload'instead」。示例里的键名从ReloadHandlerRegistry在运行时枚举得出,不再字面写死;注册表为空(纯 JVM 单测)时省略示例。约束仍是 data-driven 不依赖 modelling——两张注册表都由 data-driven 拥有,handler 由 modelling 侧注册,见JsonTreeBuilder.java:559-570、§4.6;测试JsonTreeBuilderReloadHintTest.reloadDomainContentUnderOnRegisterIsReportedWithOnReloadHint()。 - reload 侧:
on_reload列了注册域文件 → 消息尾部附「list the file under'on_register'instead」,见ReloadOrchestrator.java:441-446。
半应用保证:误列一个域,同文件另一个域仍照常处理(JsonTreeBuilderReloadHintTest.halfApplyStillDispatchesSiblingRegistrationFields())。
6. 规则出处对照¶
| 规则 | 出处(类:行 / 测试方法) |
|---|---|
| 4 段索引路径 | JsonTreeBuilder.indexDirectorySegments :745-747;JsonTreeBuilderIndexPathTest.indexDirectorySegmentsMatchCanonicalLayout |
顶层仅 on_register/on_reload |
JsonTreeBuilder.java:40,42,226-230;JsonTreeBuilderIndexSchemaTest.legacySourcesFieldIsReportedAsUnknown |
| D1 至少声明一个域 | :240-244;declaringNeitherDomainIsReportedAndConsumesNothing / emptyFileIsReportedInsteadOfSilentlyIgnored |
| D2 空数组合法 | :237;emptyArrayIsValidAndCountsAsDeclared |
| D3 兄弟字段不连累 | :256-277;nonArrayFieldDoesNotAbortTheSibling |
| D4 纯 reload 不误报 | :323-326;pureReloadModDoesNotRaiseTheAggregateError / allBrokenSchemaStillRaisesTheAggregateError |
D5 sources 非法 |
:226-230;legacySourcesFieldIsReportedAsUnknown |
| D6 双列 fail-closed | :308-316;crossListedPathIsConsumedByNeitherDomain |
| 路径条目规则 | validateSourcePath :383-392 |
| 顶层类型表 | JsonTreeBuilder.java:461-470;各 *TypeHandler.getTypeName |
blocks 字段 |
BlockTypeHandler.parse :25-34 |
items 字段 |
ItemTypeHandler.parse :22-30 |
registry_groups 字段 |
RegistryGroupHandler.parse :26-33 |
内嵌 block_entity |
BlockEntityTypeHandler :21-68;JsonTreeBuilder.java:528-535 |
| 内嵌误写顶层半应用 | JsonTreeBuilder.java:475-490;JsonTreeBuilderEmbeddedTypeDispatchTest |
| last-wins 判定 identity | EffectiveId.of :39-52;RegTypeHandler.resolveIdentity :58-60;RegistryGroupHandler.resolveIdentity :50-52;BlockEntityTypeHandler.resolveIdentity :66-68 |
| last-wins 顺序 | JsonTreeBuilder.java:105-110,655;DuplicateIdResolver.resolve :73-100;DuplicateIdResolverTest.lastMeansLastInSequenceNotLexicographicallyLastSource |
| 冲突诊断 | DuplicateIdResolver.describe :109-115;JsonTreeBuilder.java:667-674 |
| reload 两个 wrapper | StateMachineDefinitionLoader :36-102;AnimationClipLoader :42-121 |
| 目录 glob vs index-only | ReloadOrchestrator.collectGlob :348-367;AnimationClipLoader.java:29;FsmReloadHandler.globDirectories :69-71 |
| 文件级严格 / 元素级容错 | StateMachineDefinitionLoaderTest / AnimationClipLoaderTest 全部用例 |
| D9 双向提示 | JsonTreeBuilder.java:559-570;ReloadOrchestrator.java:439-446 |
| 诊断桶与门面 | JsonTreeBuilder.java:806-866;Diagnostics.java:64-166 |
7. 与其它文档的边界¶
- 本页(schema.md):JSON 格式的权威参考,覆盖字段、类型、必填、路径规则、错误分类。
intro.md:入门讲解,回答「这是什么、为什么要用」。guide-content.md:任务向教程,回答「怎么搭第一个内容文件」;其中的属性键语义、排查清单以本页格式为准。guide-extension.md:Java 侧扩展(自定义TypeHandler/ 工厂 / 属性编译器)。api-registration.md/api-extension.md/api-diagnostics.md/api-reload.md:Java API 参考;其索引章节若仍写sources,以本页为准。../../reference/fsm.md:状态机定义与动画片段元素内部的 codec 字段。
附录 A:本文未覆盖 / 待确认¶
state_machines/animation_clips元素的完整字段表与默认值:本页只规范 wrapper。元素 schema 请以对应 codec(StateMachineDefinition.CODEC/AnimationClip.CODEC)为最终依据;本页未逐字段核对。tab指向不存在的创造栏时的运行时行为:代码只把字符串解析为ResourceLocation(BlockTypeHandler.resolveCreativeTab),未在格式层校验该栏是否存在。- 数据包索引(数据包携带
on_register数据)本轮不做;注册期只读 mod jar,数据包的on_register段不会被消费。是否需要一个「快照比对 WARN」尚未实现。