Files
agentmc/mcfold/version.py

151 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""版本解析:从实例文件夹读出「该用哪个协议」。
设计依据(2026-09-25 实测):
* 实例文件夹 = 一个完整的 MC 客户端安装(与官方启动器一致)
* 版本信息在 versions/<verId>/<verId>.json
* 加载器(forge/fabric)的 verId **不是**协议版本,必须经 inheritsFrom 归一化:
minecraft-data('1.20.1') -> OK
minecraft-data('1.20.1-forge-47.4.0') -> FAIL
minecraft-data('fabric-loader-0.19.5-1.20.1') -> FAIL
* mod 加载器会改写 mainClass,这决定「谁来渲染」(见 loaders 判定)
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# 已知加载器的 mainClass 特征(用于判定实例该按哪种加载器解析 mods/)
LOADER_MAINCLASS_HINTS: tuple[tuple[str, str], ...] = (
("forgewrapper", "forge"),
("forge", "forge"),
("neoforge", "neoforge"),
("fabric", "fabric"),
("quilt", "quilt"),
)
@dataclass
class VersionInfo:
"""一个已安装版本的解析结果。"""
version_id: str # versions/ 下的目录名,如 1.20.1-forge-47.4.0
mc_version: str # 归一化后的真实 MC 版本,如 1.20.1
inherits_from: str | None # version.json 里的 inheritsFrom
main_class: str # 启动主类(加载器会改写它)
loader: str # vanilla / forge / neoforge / fabric / quilt
json_path: Path # version.json 路径
jar_path: Path | None # 客户端 jar(继承时可能在自己的目录里没有)
assets: str | None = None
libs_count: int = 0
raw: dict[str, Any] = field(default_factory=dict)
@property
def is_modded(self) -> bool:
return self.loader != "vanilla"
def _detect_loader(main_class: str, version_id: str, inherits_from: str | None) -> str:
"""从 mainClass / 版本 id 判定加载器类型。
注意:vanilla 的 version.json 里没有 inheritsFrom;加载器则一定有
(forge: inheritsFrom=1.20.1, id=1.20.1-forge-47.4.0)。
"""
lc = (main_class or "").lower()
for hint, name in LOADER_MAINCLASS_HINTS:
if hint in lc:
return name
# mainClass 不可辨识时退回看 version id(如 fabric-loader-0.19.5-1.20.1)
vid = version_id.lower()
if inherits_from:
for name in ("neoforge", "forge", "fabric", "quilt"):
if name in vid:
return name
return "unknown-modded"
return "vanilla"
def parse_version_json(path: Path) -> VersionInfo:
"""解析单个 versions/<id>/<id>.json。"""
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
version_id = data.get("id") or path.stem
inherits_from = data.get("inheritsFrom")
main_class = data.get("mainClass", "")
loader = _detect_loader(main_class, version_id, inherits_from)
# mc_version 归一化:有 inheritsFrom 就取它(这才是协议版本)
mc_version = inherits_from or version_id
jar = path.parent / f"{version_id}.jar"
return VersionInfo(
version_id=version_id,
mc_version=mc_version,
inherits_from=inherits_from,
main_class=main_class,
loader=loader,
json_path=path,
jar_path=jar if jar.is_file() else None,
assets=data.get("assets"),
libs_count=len(data.get("libraries", [])),
raw=data,
)
def list_versions(instance_dir: str | os.PathLike[str]) -> list[VersionInfo]:
"""列出实例文件夹里所有已安装版本(instances/<name>/versions/*/)。
跳过以 '.' 开头的目录,以及没有对应 json 的目录(半途安装的残留)。
"""
vroot = Path(instance_dir) / "versions"
if not vroot.is_dir():
return []
out: list[VersionInfo] = []
for d in sorted(vroot.iterdir()):
if not d.is_dir() or d.name.startswith("."):
continue
vj = d / f"{d.name}.json"
if vj.is_file():
out.append(parse_version_json(vj))
return out
def resolve_version(
instance_dir: str | os.PathLike[str],
prefer: str | None = None,
) -> VersionInfo | None:
"""从实例文件夹选出「该用哪个版本」。
prefer 指定优先的 version id(用户显式选择时用)。
否则:优先 vanilla(少一层加载器,最稳),再按目录名排序取第一个。
"""
versions = list_versions(instance_dir)
if not versions:
return None
if prefer:
for v in versions:
if v.version_id == prefer:
return v
return None
vanilla = [v for v in versions if v.loader == "vanilla"]
if vanilla:
# 有多个原版时取版本号最高的那个
return max(vanilla, key=lambda v: _verkey(v.mc_version))
return versions[0]
def _verkey(s: str) -> tuple[int, ...]:
"""把 '1.20.1' 变成可比较的元组;非数字段回退为 0。"""
parts: list[int] = []
for seg in s.split("."):
num = "".join(ch for ch in seg if ch.isdigit())
parts.append(int(num) if num else 0)
return tuple(parts)