核心 mod 与读档闸门
基本信息
| 属性 | 值 |
|---|---|
| 构建声明 | build.gradle.kts → core { coreModClass = "internal.core.CoreLoadingPlugin"; accessTransformerFile = "chunkapi_at.cfg" } |
IFMLLoadingPlugin 实现 |
com.falsepattern.chunk.internal.core.CoreLoadingPlugin |
| Mod 容器 | com.falsepattern.chunk.internal.core.ChunkAPICoreModContainer |
| 容器 modid | chunkapi + "core" = chunkapicore |
| 容器显示名 | ChunkAPI + " Core" = ChunkAPI Core |
| 父 mod | chunkapi |
| 加载接口 | IFMLLoadingPlugin + gtnhmixins.IEarlyMixinLoader(同一个类) |
| 配套文件 | Mixin 清单、Access Transformer |
⚠️ build.gradle.kts 里没有 usesMixins 键。mixin 不是通过该键启用,而是由 CoreLoadingPlugin 实现 IEarlyMixinLoader 并返回 mixin 配置名。
CoreLoadingPlugin
@IFMLLoadingPlugin.TransformerExclusions(Tags.ROOT_PKG + ".internal.core")
public class CoreLoadingPlugin implements IFMLLoadingPlugin, IEarlyMixinLoader
@TransformerExclusions 排除的是 com.falsepattern.chunk.internal.core 包——即 core mod 自身不被自己的转换器改写。
IFMLLoadingPlugin 方法 |
返回值 |
|---|---|
getASMTransformerClass() |
new String[0] —— 不提供任何 IClassTransformer,本 mod 纯靠 mixin |
getModContainerClass() |
Tags.ROOT_PKG + ".internal.core.ChunkAPICoreModContainer" |
getSetupClass() |
null |
injectData(Map<String, Object> data) |
空实现 |
getAccessTransformerClass() |
null ⚠️ |
getMixinConfig() |
"mixins.chunkapi.early.json" |
getMixins(Set<String> loadedCoreMods) |
IMixins.getEarlyMixins(Mixin.class, loadedCoreMods) |
⚠️ getAccessTransformerClass() 返回 null,但 AT 确实生效——AT 走 build.gradle.kts 的 accessTransformerFile = "chunkapi_at.cfg" 打进 jar,由构建插件(fpgradle)注册,不经 IFMLLoadingPlugin。源码里 injectData 是空方法也印证了这条路径。
晚期的 mixin 走另一个类 internal.mixin.plugin.LateMixins,标 @LateMixin,implements ILateMixinLoader,getMixinConfig() 返回 "mixins.chunkapi.late.json",getMixins(Set<String> loadedMods) 返回 IMixins.getLateMixins(Mixin.class, loadedMods)。
ChunkAPICoreModContainer
public class ChunkAPICoreModContainer extends DummyModContainer implements WorldAccessContainer
构造时 createMetadata() 建 ModMetadata:
| 字段 | 值 |
|---|---|
modId |
Tags.MOD_ID + "core" → chunkapicore |
name |
Tags.MOD_NAME + " Core" → ChunkAPI Core |
version |
Tags.MOD_VERSION |
dependencies |
new DefaultArtifactVersion(Tags.MOD_ID, Tags.MOD_VERSION) —— 依赖自己的 mod 容器 |
parent |
Tags.MOD_ID |
registerBus(EventBus bus, LoadController controller) 直接 return true,不注册任何监听器(不走 FMLCommonHandler.bus)。
实现 WorldAccessContainer 的两个方法:
| 方法 | 动作 |
|---|---|
getDataForWriting(SaveHandler, WorldInfo) |
新建 NBTTagCompound,调 DataRegistryImpl.writeLevelDat(tag),返回 |
readData(SaveHandler, WorldInfo, Map<String, NBTBase>, NBTTagCompound) |
调 DataRegistryImpl.readLevelDat(tag),返回值被丢弃 |
详见 序列化流水线 的"路径六"。
读档兼容闸门(DataRegistryImpl.readLevelDat)
这是 core mod 唯一有用户可见行为的部分。它在读档时比对 level.dat 里记录的 manager 清单与当前已注册的 manager,有任何差异就弹窗要求确认并自动备份世界。
level.dat 记录的格式
写入(writeLevelDat):
"version" = Tags.MOD_VERSION
"managers" = { "<domain:id>" : { "version": <String>, "uninstallMessage": <String> } , ... }
⚠️ 跳过所有 id 以 "minecraft:" 开头的 manager——内建 6 个 manager 不进 level.dat。
⚠️ SaveManagerInfo 只存两个字段 version 与 uninstallMessage,均为 @Nullable,写入时缺省则不写该键。读侧 getNBTStringNullable 用 tag.func_150299_b(key) == Constants.NBT.TAG_STRING 做类型化校验。readManagers 另用 tag.func_150299_b("managers") != Constants.NBT.TAG_COMPOUND 校验顶层键,遍历用 managerTag.func_150296_c()(即 SRG 的 func_150296_c(),getKeySet)。
触发条件与流程
| 步 | 动作 |
|---|---|
| 1 | !tag.hasKey("version") → newlyInstalled = true,消息:“The world you are trying to load is a vanilla world, or a world that was created with a very old version of ChunkAPI.\n”(后跟注释 // Compat code will be added here if needed.) |
| 2 | 调 verifyManagerCompatibility(tag) |
| 3 | 结果非 null → triggerScreen = true;若不是 newlyInstalled,追加消息:“The world you are trying to load was created with a different version of ChunkAPI data managers.\nData managers are used by other mods to save extra data in chunks, and changes to these data managers can cause worlds to become corrupted.\n” |
| 4 | triggerScreen 为真则追加风险警告:“ChunkAPI will attempt to load the world, but it is possible that chunks might get corrupted.\nRead the following text carefully, and make sure you understand the risks before continuing.\n\n” |
| 5 | 追加详细清单(verifyManagerCompatibility 的返回值)与:“A world backup will be automatically created in your saves directory.\n\n” |
| 6 | StartupQuery.confirm(builder.toString()) 返回 false 则 StartupQuery.abort() |
| 7 | ZipperUtil.backupWorld();抛 IOException 则 StartupQuery.notify("The world backup couldn't be created.\n\n" + e) 并 abort() |
⚠️ 第 6 步与第 7 步的顺序:先弹确认框,用户确认后才备份。备份失败会中止加载。
⚠️ readLevelDat 里 ZipperUtil.backupWorld() 的 catch (IOException e) 块没有 e.printStackTrace(),但消息里用字符串拼接 "...\n\n" + e 把异常 toString 塞进了弹窗文本。
verifyManagerCompatibility 的三类差异
| 差异 | 判定 | 报告内容 |
|---|---|---|
| 已移除的 manager | 存档里有、当前 NBTManagers 里没有 |
标题 “The following data managers are no longer present:”,逐条列 id + 存档版本,附 "Uninstall information: " + getUninstallMessage();若无信息则打印 “No uninstall information available.” |
| 新增的 manager | 当前有、存档里没有,且 id 不以 "minecraft:" 开头 |
标题 “The following data managers have been newly added:”,逐条列 id + 当前 version();newInstallDescription() 非 null 时附 "Install information: " |
| 不兼容的版本变更 | 存档有、当前也有,且 currentManager.versionChangeMessage(savedVersion) 返回非 null |
标题 “The following data managers have changed versions in an incompatible way:”,逐条 "<id> has changed versions from <旧> to <新>.\nUpgrade information: <消息>" |
返回值:任一类命中则返回 "The following managers have changed:\n" + builder,否则返回 null。
⚠️ “新增的 manager” 判定里的 !manager.id.startsWith("minecraft:") 过滤只在新增分支存在——"已移除"与"版本变更"分支没有对应过滤。但由于 writeLevelDat 本身就跳过 minecraft: 前缀,存档里不会出现内建 manager,所以过滤实际是冗余的双保险。
⚠️ verifyManagerCompatibility 的返回值类型是 String,但 readLevelDat 第 392 行写作 val managerCompat = verifyManagerCompatibility(tag); 并用 if (managerCompat != null) 判定——变量名与函数语义相反(名为 “compat” 实为"差异报告")。原文未修正。
各 manager 提供的提示文案
内建 6 个 manager 继承 VanillaManager 的默认实现(version() 返回 ""、newInstallDescription() 返回 null、uninstallMessage() 返回 ""、versionChangeMessage() 返回 null),因此全部不触发任何一类警告。第三方 manager 才可能触发。
见 内置原版数据管理器。
源码中的不一致
| 位置 | 现象 |
|---|---|
CoreLoadingPlugin.getAccessTransformerClass() |
返回 null,但 AT 通过构建配置生效——两处来源不同,易误判为"没有 AT" |
ChunkAPICoreModContainer 的 dependencies |
加了一条 DefaultArtifactVersion(Tags.MOD_ID, Tags.MOD_VERSION),即 core 容器声明依赖它自己的父 mod |
readLevelDat 第 392 行 |
val managerCompat = ... 命名与语义相反 |
writeLevelDat / readLevelDat |
只处理 NBTManagers(StorageDataManager 集合)。纯网络 manager(PacketDataManager 而非 StorageDataManager)不进 level.dat,因此新增/移除它们不触发闸门,但其数据会改变区块包体积 |
相关条目
- Mixin 清单 —
Mixin枚举的 5 个分组与两个加载器 - Access Transformer — 4 条 AT 规则
- 序列化流水线 — level.dat 的写入路径
- 数据注册表 —
writeLevelDat/readLevelDat消费的集合 - 内置原版数据管理器 —
VanillaManager的默认文案为何让内建 manager 免检