核心 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,因此新增/移除它们不触发闸门,但其数据会改变区块包体积

相关条目