FoldCraftLauncher 源码解析:fclcore 的 game 与 mod 包——版本模型、模组管理与整合包安装体系
FoldCraftLauncher 源码解析:fclcore 的 game 与 mod 包——版本模型、模组管理与整合包安装体系
本文是 FCL(Fold Craft Launcher)模块源码通读系列的第 5 篇,聚焦
FCL/src/main/java/com/tungsten/fclcore/下的game/与mod/两个包:前者是 Minecraft 版本 JSON 的建模、解析、合并与启动配置模型,后者覆盖本地模组扫描、远程模组缓存以及 CurseForge / Modrinth / MCBBS / MultiMC / Server 五种整合包格式的完整安装链路。读完本文,你将理解 FCL 如何在 Android 上复用 HMCL 的 fclcore 核心,掌握版本 JSON 的 patch 合并语义、两段式启动参数体系、版本隔离的实现点,以及整合包覆盖安装的“保留用户修改”策略与 Android 特化的兼容性处理。
game/ 包约 39 个文件、4400 行(含 tlauncher/ 子包),mod/ 包约 70 个文件、10500 行(含 curse/、modrinth/、mcbbs/、modinfo/、multimc/、server/ 子包)。二者分别回答两个问题:一个 Minecraft 版本“长什么样、怎么拼出来”,以及模组与整合包“怎么读、怎么装、怎么更新”。
一、game 包:版本 JSON 的完整建模与解析
1. Version —— 不可变的版本 JSON 模型
Version.java(513 行)是 fclcore 对官方版本 JSON(如 versions/1.20.1/1.20.1.json)的完整建模。它被设计为不可变对象:所有字段都是 final,全部 setter(setId/setJar/setPatches 等)的语义都是“复制一份并返回新实例”,例如 setId(String) 内部就是 new Version(resolved, id, ...) 重新构造(见 Version.java 第 394-396 行)。字段完整覆盖官方 JSON 结构:
| 字段 | 含义 |
|---|---|
id / version / priority |
版本标识 / patch 的版本号(如 fabric-loader 的 0.5.0.33)/ patch 优先级 |
minecraftArguments / arguments |
老式单字符串参数 / 新版两段式参数对象 |
mainClass / jar / inheritsFrom |
主类 / 使用的客户端 jar / 继承的父版本 |
assetIndex / assets |
资源索引信息 / 资源索引 id |
libraries / compatibilityRules |
依赖库列表 / 环境兼容规则 |
downloads / logging |
下载信息(JsonMap<DownloadType, DownloadInfo>)/ 日志配置 |
javaVersion / complianceLevel / minimumLauncherVersion |
Java 版本要求 / 合规级别 / 最低启动器版本 |
type / time / releaseTime |
发布类型(release/snapshot 等)/ 修改时间 / 发布时间 |
root / hidden / patches |
是否 root 壳 / 是否隐藏 / 已应用的 patch 列表 |
resolved(transient) |
是否已完成合并解析的标记 |
2. 三种存在形态:完整版、patch 与 root 壳
Version 在生命周期里会以三种形态出现:
- 完整版本 JSON:以
inheritsFrom或patches为准,是能被直接启动的形态; - patch:通过双参构造器
Version(id, version, priority, arguments, mainClass, libraries)创建(见 Version.java 第 87-89 行),只有version/priority等补丁字段,没有root,语义是“在父版本之上叠加的改动”; - root 形态(
root=true):本地versions/<id>/<id>.json里的“壳”,真实内容全部挂在patches里。FCL 保存整合包/加载器版本时正是用 root+patches 结构——壳只负责对外暴露一个版本 id,内部由一串 patch 描述完整构成。
3. resolve 流程:patch 优先级与合并语义
resolve(provider, resolvedSoFar)(见 Version.java 第 311-346 行)把三种形态统一成可启动的完整版本:
- 无
inheritsFrom:root 版本退化为new Version(id)并保留patches,同时jar缺省设为自身 id(兼容老版本 JSON 不写 jar 字段的情况); - 有
inheritsFrom:递归解析父版本。用resolvedSoFar: Set<String>记录已解析的 id 来防循环依赖——成环时只打WARNING日志("Found circular dependency versions")而不抛异常,体现“最大化兼容性”的取舍; - patches 合并:
patches按priority升序排序后逐个执行patch.setJar(null).merge(thisVersion, true),即 patch 优先级越高越晚合并、越能覆盖前面 patch 的内容。这一点是整合包编排顺序(如 Forge → OptiFine → Fabric API)能正确生效的根因。
merge(parent, isPatch)(见 Version.java 第 283-309 行)定义了字段级合并规则:
- 子非空覆盖父:
minecraftArguments/mainClass/jar/assetIndex/assets/javaVersion/downloads/logging/type/time/releaseTime等,子版本有值就用子的; arguments用Arguments.merge拼接:父子列表串联(而不是覆盖);libraries是“子在前、父在后”拼接,且不按名字去重——保证补丁库排在前面、基座库不丢失;compatibilityRules相反是父在前;minimumLauncherVersion取两者 max;- 合并结果
inheritsFrom=null、resolved=true;若自身是 patch,父的 patches 保留,否则把自己toPatch()(改 id 为"resolved."+id、置 hidden、清空 patches)并入结果。
另有 resolvePreservingPatches(provider)(见 Version.java 第 355-380 行):不真正合并,只把自身与 inheritsFrom 链上的所有版本转成 patch 挂到 root 上(addPatch 按 id 去重),供版本编辑器与需要维持 patch 结构的保存流程(如 MaintainTask.maintainPreservingPatches)使用。这也是 ModManager.refreshMods 调用 getResolvedPreservingPatchesVersion(id) 时拿到的形态。
4. 字段回退:硬编码 SHA-1 表与默认 URL
getAssetIndex()(见 Version.java 第 229-267 行):JSON 中无assetIndex时,按assets字段查一张硬编码的 SHA-1 表,覆盖1.8、14w31a、14w25a、1.7.4、1.7.10、1.7.3、pre-1.6、legacy共 8 项,URL 指向Constants.DEFAULT_INDEX_URL + hash + "/" + assetsId + ".json";getDownloadInfo()(见 Version.java 第 220-227 行):无downloads.client时按Constants.DEFAULT_VERSION_DOWNLOAD_URL/<jar>/<jar>.jar拼接默认下载 URL,jar缺省用id;Validation.validate()要求id非空(见 Version.java 第 493-511 行);equals/hashCode只看id;_setId是绕过不可变性的内部手段——refreshVersions发现目录名与 json 内 id 不一致时强改。
5. GameRepository 与 DefaultGameRepository:版本仓库与版本隔离
接口 GameRepository.java 继承 VersionProvider,约定目录结构:getVersionRoot(id) = <base>/versions/<id>,json/jar 均为 <root>/<id>.{json,jar};getRunDirectory 由 getGameDirectoryType 决定(ROOT_FOLDER=base 或 VERSION_FOLDER=版本目录)。DefaultGameRepository.java 固定返回 ROOT_FOLDER;FCL 层的 FCLGameRepository 覆写了该行为:整合包版本或 VersionSetting 开了隔离时返回 VERSION_FOLDER——这正是“版本隔离”功能的实现点。
关键行为:
refreshVersionsImpl:并行扫描versions/目录,带多层容错——若<id>.json不存在但目录里恰好只有一个 json,自动改名(连同 jar);解析失败的版本直接跳过;json 内 id 与目录名不符时以目录名为准(_setId);Classic 检测(bin/下有 lwjgl/jinput/lwjgl_util 三个 jar 即插入ClassicVersion,对应 1.5.x 以前,mainClass 为net.minecraft.client.Minecraft,用老式 minecraftArguments);最后对每个版本resolve()并按appliesToCurrentEnvironment()过滤后才入表,gameVersions缓存随之清空;readVersionJson先尝试 TLauncherVersion 再尝试官方 Version:TLauncher 格式以tlauncherVersion字段区分,其 library 存在"classifies"拼写错误,TLauncherLibrary用@SerializedName("classifies")兼容;getLibraryFile:hint=="local"时放versions/<version>/libraries/<fileName 或 artifact 路径>(MultiMC 内嵌库约定),否则放共享libraries/下按 maven 坐标展开的路径;getGameVersion:用gameVersions: ConcurrentHashMap<File,Optional<String>>缓存,底层GameVersion.minecraftVersion是纯启发式——依次尝试 jar 内version.json的 id(含" / "截断)、扫描net/minecraft/client/Minecraft.class常量池找"Minecraft Minecraft "前缀串(Beta 前缀转 b1.x)、再扫描MinecraftServer.class用"Can't keep up!"字符串往前找含数字的串;reconstructAssets:虚拟资源(index 的virtual或map_to_resources)把objects/<hash前2>/<hash>复制到assets/virtual/<assetId>/,map_to_resources时再复制到运行目录resources/;覆盖率低于 10% 判定资源缺失,回退原始目录;renameVersion:先移目录、再移 json/jar、失败回滚;更新 json 内 id/jar 字段,并遍历修复其它版本对旧 id 的inheritsFrom引用;removeVersionFromDisk:先把目录改名为<id>_removed(探测文件占用),先删 json 再删目录,保证中途失败也不会被识别为有效版本;最后异步refreshVersions;- 整合包接入点:
getModpackConfiguration(version) = versions/<id>/modpack.json,配套readModpackConfiguration/isModpack,getModManager(version)直接 newModManager(this, version); getClasspath(接口 default 方法)被 FCL 改动:硬编码跳过所有org.lwjgl库(注释"过滤 LWJGL 官方库"),因为 Android 端由宿主进程提供 LWJGL(对应 FCL/libs 下的lwjgl-3.3.3-natives-release.aar、lwjgl-3.4.1-natives-release.aar)。
6. Argument 体系:两段式启动参数
Argument接口 + 自定义 Deserializer:JSON 基本类型 →StringArgument,对象 →RuledArgument;StringArgument.toString(keys, features):正则\$\{(.*?)\}逐个替换,key 未命中时保留${...}原文(避免生成残缺命令行);RuledArgument:rules + value/values(两种字段名都支持),只有规则通过才输出;Arguments:game与jvm两个列表(新版两段式)。静态默认值DEFAULT_JVM_ARGUMENTS(含-Djava.library.path、brand、version、-cp ${classpath})、DEFAULT_GAME_ARGUMENTS(仅当has_custom_resolutionfeature 允许时输出--width/--height)。老版本minecraftArguments与新arguments并存于模型中;CompatibilityRule.appliesToCurrentEnvironment:规则列表中最后一条可应用的规则生效(无规则=允许;全部不可应用=默认 DISALLOW),feature 用Objects.equals精确匹配;- 重大移植 quirks:
OSRestriction.allow()恒返回false(见 OSRestriction.java 第 58-60 行,注释明确 Android 宿主不是 linux/windows/osx)。后果:所有带 OS 规则的 library(含 natives 变体)一律不适用;MultiMCInstancePatch里为FirstThreadOnMacOS合成的 OSX RuledArgument 实际永远不会生效。arch字段被解析但无用——这是移植期有意保留的设计,不是 bug; VersionLibraryBuilder:统一维护 mcArgs(老格式)或 game/jvm(新格式);老格式下 build 时把新格式 game 参数摊平追加进minecraftArguments并清掉arguments(模仿官方“有 minecraftArguments 就忽略 arguments”的行为);提供--tweakClass的替换/去重/前插逻辑,供 LiteLoader/OptiFine/ForgeWrapper 安装器使用。
7. Library / Artifact / 下载信息
Artifact 解析 group:name:version[:classifier][@ext],生成 maven 相对路径。Library 的 hint/filename 接受 MMC-hint/MMC-filename 别名;getDownload() 优先 downloads.artifact(natives 时取 classifiers[classifier]),URL 缺失时用 url 字段或 DEFAULT_LIBRARY_URL + path;getSha1() 把字面量 "invalid" 视为无校验(防止错误哈希阻断下载)。equals 按 name+isNative。withoutCommunityFields() 供 MultiMC 安装时生成标准布局副本。DownloadType 含 CLIENT / SERVER / WINDOWS_SERVER / 映射表。
8. World:存档解析与文件锁
- 支持目录世界与 zip 世界(zip 根直含
level.dat,或仅一个子目录);special_level.dat兼容愚人节版 20w14infinite; - NBT 读取按文件头自动识别 GZip / LZ4 Block / 未压缩(
openNBTInputStream),用 steveice10 的 openNBT 库; - 图标 icon.png 用 Android BitmapFactory 解码(
inScaled=false)——平台特化点; - 元数据访问区分版本:种子
WorldGenSettings.seed(1.16+)或RandomSeed;largeBiomes 三代格式分别处理;playerData 支持Player标签或singleplayer_uuid(IntArray)→players/data/<uuid>.dat; session.lock文件锁:lock()写入"☃"后 tryLock;isLocked对 AccessDenied/Overlapping 视为锁定;delete/copy 前检查并抛WorldLockedException;install():zip 世界解包后new World(worldDir).rename(name)同步改 LevelName 与目录名;copy排除 session.lock。getWorlds(savesDir)静态枚举。
9. 其它
GameComponentType:patch id 枚举(game/forge/fabric/quilt/neoforge/optifine/liteloader/cleanroom + legacyfabric/fabric-api 等 FCL 扩展),是 mod 包与 download 包的公共词汇;JavaVersion.kt是 FCL 简化实现:仅 isAuto/versionName/name,Auto 走JavaManager.getSuitableJavaVersion,路径为FCLPath.JAVA_PATH/<name>;LaunchOptions被 FCL 特化:Renderer(com.mio.data.Renderer)、vulkanDriverSystem、debugLog;- HMCL 的 GameDump/TxtProcessing 在本仓库不存在(全仓 grep 无命中,未被移植)。
二、mod 包:模组管理、元数据解析与五种整合包格式
1. ModManager:增删禁用恢复的完整生命周期
ModManager.java 是本地模组管理的核心(461 行)。入口 refreshMods(onScanned)(见 ModManager.java 第 219-245 行)的执行顺序:
- 先
LibraryAnalyzer.analyze(getResolvedPreservingPatchesVersion(id))拿当前 loader 集合(forge/fabric/quilt 等); - mods 目录下名为纯版本号的子目录也递归扫描(Forge 老版本会把模组按版本分目录放置);
- 按扩展名(jar/zip/litemod)从
READERS静态表选解析器(见 ModManager.java 第 67-85 行):先尝试当前环境支持的 loader 解析器,全失败再试不支持的,仍失败则构造 UNKNOWN loader、以文件名为 modid 的降级 LocalModFile(保证列表永远不崩); - zip 损坏(zipfs 抛 ZipError)时记入
brokenFiles而不炸掉整个列表(FCL 加固,见 ModManager.java 第 177-183 行); - 每个成功项回调
onScanned供 UI 增量刷新(回调在扫描线程触发,线程安全由调用方保证)。
后缀约定:.disabled / .old。disableMod/enableMod/backupMod/restoreMod 全是 Files.move 改名;LocalModFile.activeProperty 的 invalidated() 副作用就是调这两个方法——UI 开关即文件改名,改完即生效。setOld 切换文件与 localModFiles 集合;rollback 有大量前置校验(同名直接 return)。
Android 特化:addMod(Activity, Uri, name)(见 ModManager.java 第 274-290 行)从 ContentResolver.openInputStream 拷入模组文件,对应 Android 的 SAF/文档选择器流程;isFileNameMod(Uri) 校验文件类型。hasSimpleMod 同时检查启用名与 .disabled 名——整合包补全(Curse/Modrinth/Server)据此跳过已存在的文件。
2. 本地模组索引:Room 数据库取代 HMCL 文件索引
HMCL 原版基于文件的 LocalModRepository 索引在 FCL 中被 Room 数据库 RemoteModCache 取代(mod_repository_cache.db,表名仍叫 cache)。设计要点:
- key-value 缓存远程 API 原始 JSON;
- TTL 分级:指纹/单文件永久、详情 24h、版本列表 10 分钟、分类 7 天;
json=NULL表示负缓存(如指纹未命中、Modrinth 404),防止反复白查;- 命中刷新 time 实现 LRU;上限 300 条/10MB/单条 2MB;
- 首次建库时直接
deleteRecursively旧版 files/cache 目录(可重建故不迁移); - 过期即视为未命中并由 fetcher 重新拉取。
3. modinfo 子包:各 loader 的元数据解析
统一用 kotlinx.serialization(MOD_METADATA_JSON:ignoreUnknownKeys + coerceInputValues),每个解析器 fromFile(manager, path, fs) 返回 LocalModFile:
ForgeOldModMetadata:解析mcmod.info,兼容顶层数组与{modList:[...]}两种形态;作者回退链 author→authors→authorList→credits;ForgeNewModMetadata:解析META-INF/mods.toml(NeoForge 先试neoforge.mods.toml),用 tomlj 解析后转 JSON 再反序列化;${file.jarVersion}从 MANIFEST.MF 的 Implementation-Version 替换;无 toml 时解析嵌入模组(Embedded-Dependencies-Mod或 jar-in-jarMETA-INF/jarjar/metadata.json,解压到临时文件递归解析);analyzeLoader从dependencies.<modId>表的 modId=forge/neoforge 修正 loader 类型,并容忍[[dependencies]]怪写法(对应 HMCL#5068);FabricModMetadata:authors 元素兼容字符串/{name}对象;QuiltModMetadata:要求 schema_version==1,contributors 为 map;LiteModMetadata:litemod.json;PackMcMeta:资源/数据包,pack_format + min/max_format(可数组),description 递归解析 MC 文本组件颜色。
注意两个易错点:ModLoaderType 枚举拼写是 NEO_FORGED(不是 NEO_FORGE),且 FCL 加了 CLEANROOM;没有独立 ForgeLike 类——NeoForge 复用 ForgeNew 解析器仅换 loader 参数。
4. 五种整合包格式的统一骨架
所有格式共用同一套骨架:
ModpackProvider(getName/readManifest/createCompletionTask/createUpdateTask)→ Modpack(匿名子类实现 getInstallTask)→ 安装 Task(dependents: GameBuilder+ModpackInstallTask+MinecraftInstanceTask;dependencies: CompletionTask)→ ModpackConfiguration<T>(写入 versions/<id>/modpack.json,含 overrides 文件+SHA1 清单)→ ModpackUpdateTask(先备份版本目录到 backup/<id>-<随机数>,失败时恢复)。
ModpackInstallTask 的覆盖策略是精髓(见 ModpackInstallTask.java):以旧配置的 overrides 清单为基准逐文件判定:
| 情况 | 处理 |
|---|---|
| 新有旧无 | 写入 |
| 双方都有但用户已删 | 保持缺失 |
| 双方都有且内容 hash 等于旧清单 | 覆盖(未改动过) |
| 用户改过(hash 不等于旧清单) | 保留用户版 |
| 旧有新无 | 删除 |
Curse(curse/):manifest.json(manifestType=minecraftModpack)+ overrides/ + modlist.html;files 只有 projectID/fileID,无 URL 时拼 edge.forgecdn.net/files/<id/1000>/<id%1000>/<fileName>。CurseCompletionTask 用 API 反查文件名并回写 manifest.json;按项目 classId 分流(12→resourcepacks、6552→shaderpacks、其余 mods);文件不存在/反查失败→ModpackCompletionException(但继续其它下载)。CurseForgeRemoteModRepository:API key 取自 Android 资源、走 X-API-KEY 头;search 结果本地重排(归一化全等 300 万分 > 前缀 200 万 > Levenshtein 距离+词命中减分);指纹=剔除 0x09/0A/0D/20 后 MurmurHash2,两遍流式扫描防 OOM;还有两个硬编码指纹黑名单(HMCL#4597 误匹配 workaround)。
Modrinth(modrinth/):modrinth.index.json + client-overrides/(兼容 overrides/);files 带 path/hashes/env/downloads。InstallTask 读旧配置时 TypeToken 用的是 ModpackConfiguration<CurseManifest>——从 Curse 复制粘贴的痕迹,因 Gson 运行时类型擦除恰好无害(README 记录该问题已作为 bug 修复);CompletionTask 有 Unsecure path 路径穿越校验;env.client=unsupported 跳过。仓库层用 SHA-1 反查(404 归负缓存,含"HttpGetRequest 不检查状态码"的兼容注释)。
MCBBS(mcbbs/):mcbbs.packmeta(兼容 manifest.json),信息最全:Addon 列表(game/forge/fabric...版本)、AddonFile(path+sha1+force)与 CurseFile 多态(@JsonType)、LaunchInfo(launchArgument/javaArgument/minMemory/supportJava,通过 Provider.injectLaunchOptions 注入启动)、fileApi 远程源——CompletionTask 会拉 <fileApi>/manifest.json 做增量更新(force 字段决定是否覆盖用户修改),并保留已 prefetch 的 Curse 文件名(mergeFile)。LocalInstallTask 给版本加名为 "mcbbs" 的 patch 携带 manifest.libraries。导出任务同时产出 mcbbs.packmeta 与 Curse manifest.json 双格式。
MultiMC(multimc/):instance.cfg(ini,Properties 解析 + HMCL#2991 末尾冒号 workaround)、mmc-pack.json components、patches/<uid>.json 本地补丁否则拉 meta.multimc.org;requires 递归补齐缺失组件。最复杂的是 MultiMCInstancePatch.resolveArtifact:把 Json-Patch 列表有损合成为一个 "multimc" patch——末位 patch 优先、jvmArgs 累加、从 minecraftArguments 提取 --tweakClass 对转 tweakers、traits 转换(FirstThreadOnMacOS→OSX 规则的 -XstartOnFirstThread,但 OSRestriction.allow 恒 false 使其失效)、ForgeWrapper 注入 JVM 属性、compatibleJavaMajors 取可识别最大值;合成参数用老格式 minecraftArguments 承载以避免与新格式重复拼接;不支持的特性汇总告警。overrides 目录探测 4 种布局;hint=local 库双布局各拷一份;jarmods 解压合并进主 jar。
Server(server/):HMCL 自有格式 server-manifest.json + fileApi;files 直接复用 ModpackConfiguration.FileInformation;更新时 addons 变化会重建版本;下载判定考虑 mods 下 .disabled/.old 文件不重复下载;同样有 Unsecure path 校验。
5. 其它值得注意的实现
ModDependenciesResolver:BFS 解析 REQUIRED 依赖(上限 50 防病态图),兼容性以 root 的 gameVersions/loaders 交集为准(loader 精确匹配优先);去重两级——文件名零网络比对 + 项目 ID 惰性远程反查;RemoteMod.File.getIntegrityCheck优先级 md5>sha1>sha256>sha512;RemoteMod有全局 EMPTY 注册点供 BROKEN 依赖占位;- 导出任务(curse/modrinth 的 .kt):反查命中进 files、未命中进 overrides;禁用态文件的语义保留是细节——Curse 直接排除
.disabled/.old,Modrinth 则以client: optional写入索引并去掉后缀; ModManager.refreshMods依赖LibraryAnalyzer.analyze(download 包),GameBuilder/DefaultDependencyManager在 download 包——整合包安装与下载体系强耦合,mod 包只负责"格式与编排";ModpackUpdateTask的备份目录名用Math.random()*10000000探测,无清理机制(失败恢复后备份目录仍留在backup/)。
三、与其它模块的衔接
从源码结构看,game 与 mod 包不是孤岛:ModManager.refreshMods 通过 LibraryAnalyzer.analyze(download 包)确定 loader 集合;整合包安装任务依赖 download 包的 GameBuilder/DefaultDependencyManager 完成依赖下载;getClasspath 的 LWJGL 过滤则与 FCL 的启动后端(见 10-fclauncher-sdl.md)直接相关。完整的启动链路(Versions.launch → DefaultLauncher.generateCommandLine → FCLauncher.launchMinecraft)可参考系列总览 README.md。
本文对应的版本、行号与目录结构均基于仓库当前 main 分支源码(版本模型见 game 包,模组体系见 mod 包),后续若上游 HMCL 行为变化,以本仓库实际代码为准。