注意到,无论 Forge、NeoForge 还是 Fabric,模组内部结构都比较规律,有个定义元数据的文本文件。
- Fabric 存于
fabric.mod.json。具体格式: fabric.mod.json - Forge 存于
META-INF/mods.toml,1.14 之前的老版本以 json 格式存于mcmod.info,但很多老版本模组没有该文件。具体格式: mods.toml 、 The mcmod.info file - NeoForge 存于
META-INF/neoforge.mods.toml,目前兼容 Forge 的META-INF/mods.toml。具体格式: neoforge.mods.toml
个别模组比较特殊,如 Kotlin for Forge,内部没有相关配置文件。一些老版本 Forge 模组的依赖信息在 META-INF/fml_cache_annotation.json
Fabric
fabric.mod.json 需要关注的内容:
{
"id": "modid", # 模组 ID
"version": "1.0.0",
"name": "Example mod", # 模组的显示名称
"environment": "*", # * 双端、client 仅客户端、server 仅服务端
"depends": { # 模组的依赖模组/库
"fabricloader": ">=0.16.10",
"minecraft": "~1.21.5",
"java": ">=21",
"fabric-api": "*"
}
}NeoForge
neoforge.mods.toml 需要关注的内容:
[[mods]] # 必填
modId="example" # 必填
version="1.0.0" # 必填
displayName="Example Mod" # 必填
[[dependencies.example]] # 可选
modId="another" # 必填
type="required" # 必填,依赖关系,不区分大小写
# required(必需依赖),optional(可选依赖)
# incompatible(不共存),discouraged(提示警告)
versionRange="1.2.3" # 必填
ordering="NONE" # 加载顺序
# BEFORE:example 早于 another 加载
# AFTER:example 晚于 another 加载
side="BOTH" # BOTH、CLIENT、SERVERForge
在 mods.toml 中,模组属性绑定于表数组 [[mod]]。如果指定了依赖关系,则存于表数组 [[dependencies.<modid>]]。
[[mods]] # 必填
modId="example" # 必填
version="1.2.0" # 必填
displayName="Example Mod" # 必填
[[dependencies.example]] # 可选
modId = "minecraft" # 必填
mandatory = true # 必填,true 必需,false 可选
versionRange = "[1.20,)" # 必填,空字符串表示任意
ordering = "NONE" # 加载顺序,NONE 表示无所谓
side = "BOTH" # BOTH、CLIENT、SERVER
[[dependencies.example]]
modId = "another"
mandatory = true
versionRange = "[1.20,)"
ordering = "BEFORE" # example 早于 another 加载。AFTER 表示晚于而在 1.14 之前的旧版本中,如果作者写代码时注解 @Mod 的属性 useMetadata 为 true,则加载 mcmod.info 中的设置。如果 mcmod.info 中 useDependencyInformation 为 true,则以下三项生效:
- 数组
requiredMods:前置模组modid的列表。 - 数组
dependencies:在当前模组之前加载的modid列表 - 数组
dependants:在当前模组之后加载的modid列表。
DIY 模组依赖检查代码
现在只写了检查 NeoForge 和 Forge 模组中 toml 文件的代码。
点击查看
import pathlib
import zipfile
import tomllib
class Mod:
def __init__(self, file_name, name, mod_id) -> None:
self.file_name = file_name
self.name = name
self.mod_id = mod_id
self.前置 = {}
self.可选前置 = {}
self.被需要 = {}
self.被可选需要 = {}
def add_dependency(self, mod_id, side):
self.前置[mod_id] = side
def add_optional_dependency(self, mod_id, side):
self.可选前置[mod_id] = side
def add_required_by(self, mod_id, side):
self.被需要[mod_id] = side
def add_optional_required_by(self, mod_id, side):
self.被可选需要[mod_id] = side
OUTPUT_LINES: list[str] = []
MODS: dict[str, Mod] = {}
def add_line(line: str):
OUTPUT_LINES.append(line + "\n")
def output(path="./mods.txt"):
file = pathlib.Path(path)
if file.suffix == ".txt":
with file.open("w") as f:
f.writelines(OUTPUT_LINES)
else:
print("文件路径不正确")
def scan_mods_dir(dir: pathlib.Path):
for jar in dir.iterdir():
if jar.suffix == ".jar":
scan_jar(jar)
def scan_jar(jar: pathlib.Path):
dir = zipfile.Path(jar, "META-INF/")
results = [f for f in dir.glob("*mods.toml")]
if len(results) < 1:
add_line(f"{jar.name} 中未找到 mods.toml 文件")
else:
mod_file = results[0]
with mod_file.open("rb") as f:
content = tomllib.load(f)
mod_id = content["mods"][0]["modId"]
display_name = content["mods"][0]["displayName"]
mod = Mod(jar.name, display_name, mod_id)
if "dependencies" in content:
pick_dependencies(mod, content)
if mod.mod_id in MODS:
add_line(f"模组 ID 重合:{MODS[mod.mod_id]} 和 {mod.file_name}")
else:
MODS[mod.mod_id] = mod
def pick_dependencies(mod: Mod, content):
for each_mod_id, each_mod in content["dependencies"].items():
if each_mod_id != mod.mod_id:
add_line(f"{mod.file_name} 添加了 {each_mod_id} 的依赖关系")
for each_item in each_mod:
if "type" in each_item:
need_type = each_item["type"]
else:
mandatory = each_item["mandatory"]
if mandatory is True:
need_type = "required"
else:
need_type = "optional"
if each_item["modId"] not in ["minecraft", "forge", "neoforge"]:
if "side" in each_item:
if each_item["side"] == "BOTH":
side = "双端"
elif each_item["side"] == "SERVER":
side = "服务端"
else: # "CLIENT"
side = "客户端"
else:
side = "没说放哪"
if need_type == "required":
mod.add_dependency(each_item["modId"], side)
elif need_type == "optional":
mod.add_optional_dependency(each_item["modId"], side)
def seek_required():
for mod_id, mod in MODS.items():
for tmp_mod_id, side in mod.前置.items():
if tmp_mod_id in MODS:
MODS[tmp_mod_id].add_required_by(mod_id, side)
else:
add_line(f"* 没找到 {mod.file_name} 需要的 {tmp_mod_id},或许已内置")
for tmp_mod_id, side in mod.可选前置.items():
if tmp_mod_id in MODS:
MODS[tmp_mod_id].add_optional_required_by(mod_id, side)
def analysis_relations():
for mod in MODS.values():
add_line(f"{mod.name}")
add_line(f"\t{mod.file_name}")
if len(mod.前置) > 0:
add_line("前置:")
for tmp_mod_id, side in mod.前置.items():
if tmp_mod_id not in MODS:
add_line(f"\t* 需要 {tmp_mod_id},但没有文件")
else:
add_line(f"\t{side}:{MODS[tmp_mod_id].name}")
add_line(f"\t\t{MODS[tmp_mod_id].file_name}")
elif len(mod.可选前置) > 0:
add_line("可选前置:")
for tmp_mod_id, side in mod.可选前置.items():
if tmp_mod_id in MODS:
add_line(f"\t{side}:{MODS[tmp_mod_id].name}")
add_line(f"\t\t{MODS[tmp_mod_id].file_name}")
else:
add_line(f"\t{side}(未添加):{tmp_mod_id}")
elif len(mod.被需要) > 0:
add_line("需要本模组:")
for tmp_mod_id, side in mod.被需要.items():
add_line(f"\t{side}:{MODS[tmp_mod_id].name}")
add_line(f"\t\t{MODS[tmp_mod_id].file_name}")
elif len(mod.被可选需要) > 0:
add_line("可选需要本模组:")
for tmp_mod_id, side in mod.被可选需要.items():
add_line(f"\t{side}:{MODS[tmp_mod_id].name}")
add_line(f"\t\t{MODS[tmp_mod_id].file_name}")
else:
add_line("独立模组")
add_line("\n")
mods_dir = input()
mods_dir = pathlib.Path(mods_dir)
scan_mods_dir(mods_dir)
seek_required()
add_line("<---------------->\n")
analysis_relations()
output()